diff --git a/backend/package-lock.json b/backend/package-lock.json index 4f422adb..b37210d8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -554,9 +554,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -567,13 +567,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -584,13 +584,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -601,13 +601,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -618,13 +618,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -635,13 +635,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -652,13 +652,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -669,13 +669,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -686,13 +686,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -703,13 +703,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -720,13 +720,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -737,13 +737,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -754,13 +754,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -771,13 +771,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -788,13 +788,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -805,13 +805,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -822,13 +822,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -839,13 +839,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -856,13 +873,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -873,13 +907,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -890,13 +941,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -907,13 +958,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -924,13 +975,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -941,7 +992,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -2477,23 +2528,23 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", + "bytes": "~3.1.2", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -2515,21 +2566,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -2876,9 +2912,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -3153,9 +3189,9 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -3218,9 +3254,9 @@ } }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3228,32 +3264,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -3554,45 +3593,49 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -3625,21 +3668,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3745,17 +3773,17 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -3878,7 +3906,6 @@ "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, @@ -3978,19 +4005,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4147,19 +4161,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/human-signals": { @@ -5278,10 +5296,13 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -5655,9 +5676,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -5910,10 +5931,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -5967,15 +5987,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -6071,16 +6091,6 @@ "node": ">=4" } }, - "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/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -6183,24 +6193,24 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -6222,15 +6232,15 @@ "license": "MIT" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" @@ -6418,9 +6428,9 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6706,14 +6716,13 @@ } }, "node_modules/tsx": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.2.tgz", - "integrity": "sha512-BCNd4kz6fz12fyrgCTEdZHGJ9fWTGeUzXmQysh0RVocDY3h4frk05ZNCXSy4kIenF7y/QnrdiVpTsyNRn6vlAw==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.19.10", - "get-tsconfig": "^4.7.2" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" diff --git a/backend/prisma/migrations/20260527000000_add_share_price_snapshots/migration.sql b/backend/prisma/migrations/20260527000000_add_share_price_snapshots/migration.sql new file mode 100644 index 00000000..ebcd3796 --- /dev/null +++ b/backend/prisma/migrations/20260527000000_add_share_price_snapshots/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "SharePriceSnapshot" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "sharePrice" TEXT NOT NULL, + "recordedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "ledgerSeq" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE INDEX "SharePriceSnapshot_recordedAt_idx" ON "SharePriceSnapshot"("recordedAt"); + +-- CreateIndex +CREATE INDEX "SharePriceSnapshot_ledgerSeq_idx" ON "SharePriceSnapshot"("ledgerSeq"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 8c364399..6a8aa221 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -24,6 +24,17 @@ model VaultState { updatedAt DateTime @updatedAt } +model SharePriceSnapshot { + id Int @id @default(autoincrement()) + sharePrice String + recordedAt DateTime @default(now()) + ledgerSeq Int? + createdAt DateTime @default(now()) + + @@index([recordedAt]) + @@index([ledgerSeq]) +} + model Transaction { id String @id @default(uuid()) user String diff --git a/backend/src/__tests__/adminFeatures.test.ts b/backend/src/__tests__/adminFeatures.test.ts index 2935d560..776beabe 100644 --- a/backend/src/__tests__/adminFeatures.test.ts +++ b/backend/src/__tests__/adminFeatures.test.ts @@ -7,6 +7,7 @@ import { resetAuditLogs } from '../auditLog'; describe('Admin backend features', () => { const adminKey = 'admin-feature-test-key'; const authHeader = { Authorization: `ApiKey ${adminKey}` }; + const walletAddress = 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567'; beforeAll(() => { registerApiKey(adminKey); @@ -36,12 +37,17 @@ describe('Admin backend features', () => { expect(webhookResponse.status).toBe(201); + await request(app) + .post('/admin/allowlist/add') + .set(authHeader) + .send({ walletAddress, reason: 'admin-feature-test' }); + const depositResponse = await request(app) .post('/api/v1/vault/deposits') .send({ amount: '125.00', asset: 'USDC', - walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567', + walletAddress, }); expect(depositResponse.status).toBe(201); diff --git a/backend/src/__tests__/eventPollingService.test.ts b/backend/src/__tests__/eventPollingService.test.ts index 49f7c9ec..4cfe2e5a 100644 --- a/backend/src/__tests__/eventPollingService.test.ts +++ b/backend/src/__tests__/eventPollingService.test.ts @@ -1,5 +1,6 @@ import { EventPollingService } from '../eventPollingService'; import { getPrismaClient } from '../prismaClient'; +import { createStellarRpcFetchMock } from './mocks/stellarRpc'; const prisma = getPrismaClient(); @@ -349,4 +350,169 @@ describe('EventPollingService', () => { expect(true).toBe(true); }); }); + + describe('Failure and Gap-Recovery Scenarios', () => { + it('retries after an RPC timeout without dropping events', async () => { + let cursor = 1000; + const storedEvents = new Set(); + let eventsCallCount = 0; + + (prisma.eventCursor.findUnique as jest.Mock).mockImplementation(async () => ({ + id: 1, + lastLedgerSeq: cursor, + })); + + (prisma.eventCursor.upsert as jest.Mock).mockImplementation(async ({ update }) => { + cursor = update.lastLedgerSeq; + return { id: 1, lastLedgerSeq: cursor }; + }); + + (prisma.processedEvent.findUnique as jest.Mock).mockImplementation(async ({ where }) => { + return storedEvents.has(where.id) ? { id: where.id } : null; + }); + + (prisma.processedEvent.upsert as jest.Mock).mockImplementation(async ({ create }) => { + storedEvents.add(create.id); + return create; + }); + + global.fetch = createStellarRpcFetchMock(async ({ method }) => { + if (method === 'getLatestLedger') { + return { result: { sequence: 1002 } }; + } + + eventsCallCount += 1; + if (eventsCallCount === 1) { + throw new Error('RPC timeout'); + } + + return { + result: { + events: [ + { + id: 'event-1001', + type: 'contract', + ledger: 1001, + contractId: 'CTEST123', + txHash: 'tx-1001', + }, + { + id: 'event-1002', + type: 'contract', + ledger: 1002, + contractId: 'CTEST123', + txHash: 'tx-1002', + }, + ], + }, + }; + }); + + await (service as any).pollEvents(); + expect(cursor).toBe(1000); + expect(prisma.processedEvent.upsert).not.toHaveBeenCalled(); + + await (service as any).pollEvents(); + + expect(prisma.processedEvent.upsert).toHaveBeenCalledTimes(2); + expect(cursor).toBe(1002); + expect(storedEvents.has('event-1001')).toBe(true); + expect(storedEvents.has('event-1002')).toBe(true); + }); + + it('re-fetches all missing ledger ranges during replay', async () => { + const requestedRanges: Array<{ start: number; end: number }> = []; + const replayService = new EventPollingService({ + ...mockConfig, + batchSize: 100, + }); + + (prisma.eventCursor.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + lastLedgerSeq: 1000, + }); + + (prisma.eventCursor.upsert as jest.Mock).mockResolvedValue({}); + (prisma.processedEvent.findUnique as jest.Mock).mockResolvedValue(null); + + global.fetch = createStellarRpcFetchMock(async ({ method, params }) => { + if (method === 'getLatestLedger') { + return { result: { sequence: 1200 } }; + } + + requestedRanges.push({ + start: params.startLedger, + end: params.startLedger + 99, + }); + + return { + result: { + events: [], + }, + }; + }); + + await replayService.start(); + await replayService.stop(); + + expect(requestedRanges).toEqual([ + { start: 1001, end: 1100 }, + { start: 1101, end: 1200 }, + ]); + expect(prisma.eventCursor.upsert).toHaveBeenCalledTimes(2); + }); + + it('handles duplicate event delivery idempotently with one DB record', async () => { + const storedEvents = new Set(); + + (prisma.eventCursor.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + lastLedgerSeq: 1000, + }); + + (prisma.eventCursor.upsert as jest.Mock).mockResolvedValue({}); + + (prisma.processedEvent.findUnique as jest.Mock).mockImplementation(async ({ where }) => { + return storedEvents.has(where.id) ? { id: where.id } : null; + }); + + (prisma.processedEvent.upsert as jest.Mock).mockImplementation(async ({ create }) => { + storedEvents.add(create.id); + return create; + }); + + global.fetch = createStellarRpcFetchMock(async ({ method }) => { + if (method === 'getLatestLedger') { + return { result: { sequence: 1010 } }; + } + + return { + result: { + events: [ + { + id: 'duplicate-event', + type: 'contract', + ledger: 1005, + contractId: 'CTEST123', + txHash: 'tx-dup', + }, + { + id: 'duplicate-event', + type: 'contract', + ledger: 1005, + contractId: 'CTEST123', + txHash: 'tx-dup', + }, + ], + }, + }; + }); + + await service.start(); + + expect(prisma.processedEvent.upsert).toHaveBeenCalledTimes(1); + expect(storedEvents.size).toBe(1); + expect(storedEvents.has('duplicate-event')).toBe(true); + }); + }); }); diff --git a/backend/src/__tests__/governance.test.ts b/backend/src/__tests__/governance.test.ts index e83f1a3d..027788e9 100644 --- a/backend/src/__tests__/governance.test.ts +++ b/backend/src/__tests__/governance.test.ts @@ -33,6 +33,11 @@ describe('Backend governance', () => { walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567', }; + await request(app) + .post('/admin/allowlist/add') + .set('Authorization', `ApiKey ${adminApiKey}`) + .send({ walletAddress: payload.walletAddress, reason: 'governance-test' }); + const first = await request(app) .post('/api/v1/vault/deposits') .set('x-idempotency-key', 'deposit-key-1') @@ -50,13 +55,18 @@ describe('Backend governance', () => { }); it('rejects conflicting requests that reuse the same idempotency key', async () => { + await request(app) + .post('/admin/allowlist/add') + .set('Authorization', `ApiKey ${adminApiKey}`) + .send({ walletAddress: targetWallet, reason: 'governance-test' }); + const first = await request(app) .post('/api/v1/vault/deposits') .set('x-idempotency-key', 'deposit-key-2') .send({ amount: 250, asset: 'USDC', - walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567', + walletAddress: targetWallet, }); const second = await request(app) @@ -65,7 +75,7 @@ describe('Backend governance', () => { .send({ amount: 300, asset: 'USDC', - walletAddress: 'GABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz234567', + walletAddress: targetWallet, }); expect(first.status).toBe(201); diff --git a/backend/src/__tests__/latencyMonitoring.test.ts b/backend/src/__tests__/latencyMonitoring.test.ts index 949b011c..1a073e5c 100644 --- a/backend/src/__tests__/latencyMonitoring.test.ts +++ b/backend/src/__tests__/latencyMonitoring.test.ts @@ -302,7 +302,7 @@ describe('LatencyMonitoringService', () => { freshService.recordLatency(endpoint, 500); const metricsBefore = freshService.getDetailedMetrics(); - const metricBefore = metricsBefore.find(m => m.endpoint === endpoint); + const metricBefore = metricsBefore.find((m: any) => m.endpoint === endpoint); expect(metricBefore?.currentP95).toBe(500); expect(metricBefore?.isBreaching).toBe(true); @@ -310,7 +310,7 @@ describe('LatencyMonitoringService', () => { return new Promise((resolve) => { setTimeout(() => { const metricsAfter = freshService.getDetailedMetrics(); - const metricAfter = metricsAfter.find(m => m.endpoint === endpoint); + const metricAfter = metricsAfter.find((m: any) => m.endpoint === endpoint); // Stale data should be pruned, P95 should drop to 0 expect(metricAfter?.currentP95).toBe(0); expect(metricAfter?.isBreaching).toBe(false); @@ -337,7 +337,7 @@ describe('LatencyMonitoringService', () => { freshService.recordLatency(endpoint, 100); const metrics = freshService.getDetailedMetrics(); - const metric = metrics.find(m => m.endpoint === endpoint); + const metric = metrics.find((m: any) => m.endpoint === endpoint); // Only the fresh data point should count expect(metric?.dataPoints).toBe(1); expect(metric?.currentP95).toBe(100); diff --git a/backend/src/__tests__/mocks/stellarRpc.ts b/backend/src/__tests__/mocks/stellarRpc.ts new file mode 100644 index 00000000..330ab88e --- /dev/null +++ b/backend/src/__tests__/mocks/stellarRpc.ts @@ -0,0 +1,18 @@ +type RpcMethod = 'getLatestLedger' | 'getEvents'; + +interface RpcRequest { + method: RpcMethod; + params?: any; +} + +type RpcHandler = (request: RpcRequest) => Promise; + +export function createStellarRpcFetchMock(handler: RpcHandler): jest.Mock { + return jest.fn(async (_url: string, options: any) => { + const body = JSON.parse(options.body || '{}'); + const payload = await handler({ method: body.method, params: body.params }); + return { + json: async () => payload, + }; + }); +} \ No newline at end of file diff --git a/backend/src/__tests__/rateLimiter.property.test.ts b/backend/src/__tests__/rateLimiter.property.test.ts index b86328b9..01d7aa95 100644 --- a/backend/src/__tests__/rateLimiter.property.test.ts +++ b/backend/src/__tests__/rateLimiter.property.test.ts @@ -1,15 +1,24 @@ /** * @file rateLimiter.property.test.ts - * Property-based tests for the Redis-backed rate limiter. - * - * Uses fast-check to verify universal correctness properties across - * generated inputs. Each property runs a minimum of 100 iterations. + * Property-style tests for the Redis-backed rate limiter. */ -import * as fc from 'fast-check'; import express, { Request, Response } from 'express'; import request from 'supertest'; +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function randomAlphaNumeric(length: number): string { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let out = ''; + for (let i = 0; i < length; i++) { + out += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return out; +} + // Helper: build a minimal express app with a fresh in-memory limiter function buildApp(max: number, windowMs = 60000) { jest.resetModules(); @@ -29,41 +38,28 @@ function buildApp(max: number, windowMs = 60000) { // Feature: redis-rate-limiting, Property 5: 429 headers and body describe('Property 5: Requests beyond the limit receive 429 with required headers and body', () => { it('holds for randomly generated limit values', async () => { - await fc.assert( - fc.asyncProperty( - fc.integer({ min: 1, max: 20 }), - async (limit) => { - const app = buildApp(limit); - const key = `wallet-p5-${limit}-${Math.random()}`; - - // Make exactly `limit` requests (all should succeed) - for (let i = 0; i < limit; i++) { - await request(app).get('/test').set('x-api-key', key); - } - - // The (limit+1)th request must be 429 - const res = await request(app).get('/test').set('x-api-key', key); - - if (res.status !== 429) return false; - - // Required headers - if (!res.headers['retry-after']) return false; - if (!res.headers['ratelimit-limit']) return false; - if (!res.headers['ratelimit-remaining']) return false; - if (!res.headers['ratelimit-reset']) return false; - - // Required body fields - const body = res.body as Record; - if (!body.error) return false; - if (body.status !== 429) return false; - if (!body.message) return false; - if (typeof body.retryAfter !== 'number') return false; - - return true; - } - ), - { numRuns: 10 } // keep fast; each run makes limit+1 HTTP requests - ); + for (let run = 0; run < 10; run++) { + const limit = randomInt(1, 20); + const app = buildApp(limit); + const key = `wallet-p5-${limit}-${run}-${Date.now()}`; + + for (let i = 0; i < limit; i++) { + await request(app).get('/test').set('x-api-key', key); + } + + const res = await request(app).get('/test').set('x-api-key', key); + expect(res.status).toBe(429); + expect(res.headers['retry-after']).toBeTruthy(); + expect(res.headers['ratelimit-limit']).toBeTruthy(); + expect(res.headers['ratelimit-remaining']).toBeTruthy(); + expect(res.headers['ratelimit-reset']).toBeTruthy(); + + const body = res.body as Record; + expect(body.error).toBeTruthy(); + expect(body.status).toBe(429); + expect(body.message).toBeTruthy(); + expect(typeof body.retryAfter).toBe('number'); + } }); }); @@ -72,26 +68,20 @@ describe('Property 5: Requests beyond the limit receive 429 with required header // Feature: redis-rate-limiting, Property 6: 200 includes rate-limit headers describe('Property 6: Requests within the limit include rate-limit headers', () => { it('holds for randomly generated request counts within limit', async () => { - await fc.assert( - fc.asyncProperty( - fc.integer({ min: 1, max: 10 }), - async (count) => { - const limit = count + 5; // ensure we stay within limit - const app = buildApp(limit); - const key = `wallet-p6-${count}-${Math.random()}`; - - for (let i = 0; i < count; i++) { - const res = await request(app).get('/test').set('x-api-key', key); - if (res.status !== 200) return false; - if (!res.headers['ratelimit-limit']) return false; - if (!res.headers['ratelimit-remaining']) return false; - if (!res.headers['ratelimit-reset']) return false; - } - return true; - } - ), - { numRuns: 10 } - ); + for (let run = 0; run < 10; run++) { + const count = randomInt(1, 10); + const limit = count + 5; + const app = buildApp(limit); + const key = `wallet-p6-${count}-${run}-${Date.now()}`; + + for (let i = 0; i < count; i++) { + const res = await request(app).get('/test').set('x-api-key', key); + expect(res.status).toBe(200); + expect(res.headers['ratelimit-limit']).toBeTruthy(); + expect(res.headers['ratelimit-remaining']).toBeTruthy(); + expect(res.headers['ratelimit-reset']).toBeTruthy(); + } + } }); }); @@ -100,23 +90,18 @@ describe('Property 6: Requests within the limit include rate-limit headers', () // Feature: redis-rate-limiting, Property 7: Counter initialises to 1 describe('Property 7: Counter initialises to 1 on first request in a window', () => { it('RateLimit-Remaining equals limit-1 after first request', async () => { - await fc.assert( - fc.asyncProperty( - fc.integer({ min: 2, max: 30 }), - fc.string({ minLength: 5, maxLength: 20 }), - async (limit, walletSuffix) => { - const app = buildApp(limit); - const key = `wallet-p7-${walletSuffix}-${Math.random()}`; - - const res = await request(app).get('/test').set('x-api-key', key); - if (res.status !== 200) return false; - - const remaining = parseInt(res.headers['ratelimit-remaining'] as string, 10); - return remaining === limit - 1; - } - ), - { numRuns: 10 } - ); + for (let run = 0; run < 10; run++) { + const limit = randomInt(2, 30); + const walletSuffix = randomAlphaNumeric(randomInt(5, 20)); + const app = buildApp(limit); + const key = `wallet-p7-${walletSuffix}-${run}`; + + const res = await request(app).get('/test').set('x-api-key', key); + expect(res.status).toBe(200); + + const remaining = parseInt(res.headers['ratelimit-remaining'] as string, 10); + expect(remaining).toBe(limit - 1); + } }); }); @@ -132,46 +117,41 @@ describe('Property 8: Rate-limit log entries contain required fields without exp }); it('log contains required fields and masks wallet in production', async () => { - await fc.assert( - fc.asyncProperty( - fc.string({ minLength: 10, maxLength: 40 }), - async (walletAddress) => { - process.env = { ...originalEnv, NODE_ENV: 'production' }; - jest.resetModules(); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { createLimiter } = require('../rateLimiter'); - - const logEntries: Record[] = []; - const consoleSpy = jest.spyOn(console, 'log').mockImplementation((msg: string) => { - try { logEntries.push(JSON.parse(msg)); } catch { /* ignore non-JSON */ } - }); - - const app = express(); - app.use(express.json()); - const limiter = createLimiter({ routePrefix: '/prop-p8', max: 1, windowMs: 60000 }); - app.get('/test', limiter, (_req: Request, res: Response) => res.json({ ok: true })); - - // First request succeeds, second triggers 429 log - await request(app).get('/test').set('x-wallet-address', walletAddress); - await request(app).get('/test').set('x-wallet-address', walletAddress); - - consoleSpy.mockRestore(); - - const rateLimitedLog = logEntries.find((e) => e.event === 'rate_limited'); - if (!rateLimitedLog) return false; - - // Required fields present - if (!rateLimitedLog.path) return false; - if (rateLimitedLog.resetTime === undefined) return false; - if (!rateLimitedLog.key) return false; - - // Full wallet address must NOT appear in log key in production - if (walletAddress.length > 8 && rateLimitedLog.key === walletAddress) return false; - - return true; + for (let run = 0; run < 10; run++) { + const walletAddress = randomAlphaNumeric(randomInt(10, 40)); + + process.env = { ...originalEnv, NODE_ENV: 'production' }; + jest.resetModules(); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { createLimiter } = require('../rateLimiter'); + + const logEntries: Record[] = []; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation((msg: string) => { + try { + logEntries.push(JSON.parse(msg)); + } catch { + // Ignore non-JSON log lines. } - ), - { numRuns: 10 } - ); + }); + + const app = express(); + app.use(express.json()); + const limiter = createLimiter({ routePrefix: '/prop-p8', max: 1, windowMs: 60000 }); + app.get('/test', limiter, (_req: Request, res: Response) => res.json({ ok: true })); + + await request(app).get('/test').set('x-wallet-address', walletAddress); + await request(app).get('/test').set('x-wallet-address', walletAddress); + + consoleSpy.mockRestore(); + + const rateLimitedLog = logEntries.find((e) => e.event === 'rate_limited'); + expect(rateLimitedLog).toBeTruthy(); + expect(rateLimitedLog?.path).toBeTruthy(); + expect(rateLimitedLog?.resetTime).toBeDefined(); + expect(rateLimitedLog?.key).toBeTruthy(); + if (walletAddress.length > 8) { + expect(rateLimitedLog?.key).not.toBe(walletAddress); + } + } }); }); diff --git a/backend/src/__tests__/referral.test.ts b/backend/src/__tests__/referral.test.ts index 53752ed0..96699d11 100644 --- a/backend/src/__tests__/referral.test.ts +++ b/backend/src/__tests__/referral.test.ts @@ -1,7 +1,7 @@ import request from 'supertest'; import app from '../index'; import { getPrismaClient, disconnectPrismaClient } from '../prismaClient'; -import { referralService } from '../referralService'; +import Decimal from 'decimal.js'; // Use the centralized Prisma Client instance const getPrisma = () => getPrismaClient(); @@ -9,7 +9,29 @@ const getPrisma = () => getPrismaClient(); describe('Referral System Integration', () => { const referrerWallet = 'G_REFERRER_WALLET_ADDRESS'; const referredWallet = 'G_REFERRED_WALLET_ADDRESS'; + const secondReferredWallet = 'G_REFERRED_WALLET_ADDRESS_2'; + const nonProfitableWallet = 'G_REFERRED_WALLET_ADDRESS_3'; const referralCode = 'WELCOME2026'; + const rewardRate = new Decimal(process.env.REFERRAL_REWARD_PERCENTAGE || '0.05'); + let baseReferralReward = new Decimal(0); + + const createTransaction = async ( + user: string, + amount: string, + type: 'deposit' | 'withdrawal', + timestamp: string, + ) => { + const prisma = getPrisma(); + await prisma.transaction.create({ + data: { + user, + amount, + type, + referralCode, + timestamp: new Date(timestamp), + }, + }); + }; beforeAll(async () => { // Clear relevant data @@ -17,9 +39,68 @@ describe('Referral System Integration', () => { await prisma.referral.deleteMany(); await prisma.referralCode.deleteMany(); await prisma.transaction.deleteMany(); + await prisma.sharePriceSnapshot.deleteMany(); + + await prisma.sharePriceSnapshot.createMany({ + data: [ + { + sharePrice: '1.000000', + recordedAt: new Date('2026-01-01T00:00:00.000Z'), + ledgerSeq: 100, + }, + { + sharePrice: '1.200000', + recordedAt: new Date('2026-01-10T00:00:00.000Z'), + ledgerSeq: 200, + }, + { + sharePrice: '1.250000', + recordedAt: new Date('2026-01-20T00:00:00.000Z'), + ledgerSeq: 300, + }, + ], + }); // Setup referral code - await referralService.createReferralCode(referrerWallet, referralCode); + await prisma.referralCode.create({ + data: { + code: referralCode, + ownerAddress: referrerWallet, + }, + }); + + // Seed one additional referral with explicit ledger-backed transaction history. + await prisma.referral.create({ + data: { + referrerAddress: referrerWallet, + referredAddress: secondReferredWallet, + firstDepositAt: new Date('2026-01-01T00:00:00.000Z'), + }, + }); + await createTransaction(secondReferredWallet, '100', 'deposit', '2026-01-02T00:00:00.000Z'); + await createTransaction(secondReferredWallet, '20', 'withdrawal', '2026-01-12T00:00:00.000Z'); + + // Expected second wallet yield: + // shares = 100/1.0 - 20/1.2 = 83.3333333333... + // ending value = shares * 1.25 = 104.1666666666... + // net yield = ending + withdrawn - deposited = 24.1666666666... + // reward = 24.1666666666... * 0.05 = 1.2083333333... + const wallet2Yield = new Decimal('100') + .div('1') + .minus(new Decimal('20').div('1.2')) + .mul('1.25') + .plus('20') + .minus('100'); + baseReferralReward = wallet2Yield.mul(rewardRate).toDecimalPlaces(6, Decimal.ROUND_HALF_UP); + + await prisma.referral.create({ + data: { + referrerAddress: referrerWallet, + referredAddress: nonProfitableWallet, + firstDepositAt: new Date('2026-01-01T00:00:00.000Z'), + }, + }); + await createTransaction(nonProfitableWallet, '50', 'deposit', '2026-01-20T00:00:00.000Z'); }); afterAll(async () => { @@ -81,13 +162,8 @@ describe('Referral System Integration', () => { const response = await request(app).get(`/api/v1/referrals/${referrerWallet}`); expect(response.status).toBe(200); - expect(response.body).toHaveProperty('referral_count', 1); - - // Based on our mock yield calculation (10% gain, 5% reward): - // Total deposited: 1000 + 500 = 1500 - // Yield: 1500 * 0.1 = 150 - // Reward: 150 * 0.05 = 7.5 - expect(response.body.total_reward_earned).toBe('7.500000'); + expect(response.body).toHaveProperty('referral_count', 3); + expect(response.body.total_reward_earned).toBe(baseReferralReward.toFixed(6)); }); it('should return 404 for wallet with no referral activity', async () => { @@ -97,30 +173,38 @@ describe('Referral System Integration', () => { }); describe('Reward Calculation Precision', () => { - it('should handle small yield values with precision', async () => { + it('should handle small yields with deterministic precision from snapshots', async () => { const smallReferredWallet = 'G_SMALL_REFERRED'; + const prisma = getPrisma(); + + await prisma.referral.create({ + data: { + referrerAddress: referrerWallet, + referredAddress: smallReferredWallet, + firstDepositAt: new Date('2026-01-01T00:00:00.000Z'), + }, + }); - // Record small deposit - await request(app) - .post('/api/v1/vault/deposits') - .send({ - amount: '0.012345', - asset: 'USDC', - walletAddress: smallReferredWallet, - referralCode: referralCode, - }); + await createTransaction(smallReferredWallet, '0.012345', 'deposit', '2026-01-02T00:00:00.000Z'); const response = await request(app).get(`/api/v1/referrals/${referrerWallet}`); - - // Previous 7.5 + (0.012345 * 0.1 * 0.05) - // 0.012345 * 0.005 = 0.000061725 - // 7.5 + 0.000061725 = 7.500061725 -> 7.500062 (rounded to 6 places in simulation maybe) - // Actually our simulate calculation rounds yield to 6 places then multiplies - // yield = 0.012345 * 0.1 = 0.0012345 -> rounded 0.001235 - // reward = 0.001235 * 0.05 = 0.00006175 - // total = 7.5 + 0.00006175 = 7.50006175 - + + const smallWalletYield = new Decimal('0.012345').mul(new Decimal('1.25').minus('1')); + const smallWalletReward = smallWalletYield.mul(rewardRate); + const expectedReward = baseReferralReward + .plus(smallWalletReward) + .toDecimalPlaces(6, Decimal.ROUND_HALF_UP); + + expect(response.body.total_reward_earned).toBe(expectedReward.toFixed(6)); expect(response.body.total_reward_earned).toMatch(/^\d+\.\d{6}$/); }); + + it('should ignore referrals with zero or negative net yield', async () => { + const response = await request(app).get(`/api/v1/referrals/${referrerWallet}`); + + // Non-profitable wallet contributes 0 reward even though it has referral + tx history. + expect(response.status).toBe(200); + expect(response.body.total_reward_earned).toBe('1.208488'); + }); }); }); diff --git a/backend/src/eventPollingService.ts b/backend/src/eventPollingService.ts index 8d4a26cc..b99a0fd9 100644 --- a/backend/src/eventPollingService.ts +++ b/backend/src/eventPollingService.ts @@ -255,7 +255,7 @@ export class EventPollingService { endLedger, error: error instanceof Error ? error.message : 'Unknown error', }); - return []; + throw error; } } } diff --git a/backend/src/integration-test.ts b/backend/src/integration-test.ts index 3e372940..35c8780a 100644 --- a/backend/src/integration-test.ts +++ b/backend/src/integration-test.ts @@ -51,7 +51,7 @@ async function testLatencyMonitoring() { latencyMonitoringService.recordLatency('/api/v1/vault/deposit', 800); metrics = latencyMonitoringService.getDetailedMetrics(); - let depositMetric = metrics.find(m => m.endpoint === '/api/v1/vault/deposit'); + const depositMetric = metrics.find(m => m.endpoint === '/api/v1/vault/deposit'); console.log(`✅ Deposit endpoint P95: ${depositMetric?.currentP95}ms`); console.log(`✅ Deposit SLO Threshold: ${depositMetric?.threshold}ms`); @@ -63,7 +63,7 @@ async function testLatencyMonitoring() { latencyMonitoringService.recordLatency('/api/v1/vault/67890', 350); metrics = latencyMonitoringService.getDetailedMetrics(); - let dynamicMetric = metrics.find(m => m.endpoint === '/api/v1/vault/:id'); + const dynamicMetric = metrics.find(m => m.endpoint === '/api/v1/vault/:id'); console.log(`✅ Normalized endpoint: ${dynamicMetric?.endpoint}`); console.log(`✅ Dynamic metric P95: ${dynamicMetric?.currentP95}ms`); diff --git a/backend/src/latencyMonitoring.ts b/backend/src/latencyMonitoring.ts index f21f8c8f..ef45b4cf 100644 --- a/backend/src/latencyMonitoring.ts +++ b/backend/src/latencyMonitoring.ts @@ -222,12 +222,12 @@ export class LatencyMonitoringService { private normalizeEndpoint(endpoint: string): string { // Convert dynamic paths like /api/v1/vault/123 to /api/v1/vault/:id - if (endpoint.match(/^\/api\/v1\/vault\/[^\/]+$/)) { + if (endpoint.match(/^\/api\/v1\/vault\/[^/]+$/)) { return '/api/v1/vault/:id'; } // Handle other dynamic patterns like /api/v1/resource/123 - const match = endpoint.match(/^\/api\/v1\/([^\/]+)\/[^\/]+$/); + const match = endpoint.match(/^\/api\/v1\/([^/]+)\/[^/]+$/); if (match) { return `/api/v1/${match[1]}/:id`; } diff --git a/backend/src/pagination.ts b/backend/src/pagination.ts index c46f10cd..21c8b6a3 100644 --- a/backend/src/pagination.ts +++ b/backend/src/pagination.ts @@ -164,7 +164,6 @@ export function paginateWithCursor( ): { data: T[]; pagination: PaginationMeta } { const limit = query.limit || DEFAULT_PAGINATION_CONFIG.defaultLimit; let startIndex = 0; - let invalidCursor = false; if (query.page && query.page > 0) { startIndex = (query.page - 1) * limit; @@ -194,18 +193,6 @@ export function paginateWithCursor( startIndex = cursorIndex + 1; } - if (invalidCursor) { - return { - data: [], - pagination: { - count: 0, - total: items.length, - hasNextPage: false, - hasPrevPage: false, - }, - }; - } - // Extract page items const pageItems = items.slice(startIndex, startIndex + limit + 1); const hasMore = pageItems.length > limit; diff --git a/backend/src/referralService.ts b/backend/src/referralService.ts index 70290f01..caebc752 100644 --- a/backend/src/referralService.ts +++ b/backend/src/referralService.ts @@ -1,13 +1,36 @@ -import { PrismaClient } from '@prisma/client'; import { getPrismaClient } from './prismaClient'; -import Decimal from 'decimal.js'; import { logger } from './middleware/structuredLogging'; -// Use the centralized Prisma Client instance const getPrisma = () => getPrismaClient(); -// Configurable reward percentage (default 5% if not set) -const REFERRAL_REWARD_PERCENTAGE = new Decimal(process.env.REFERRAL_REWARD_PERCENTAGE || '0.05'); +const REFERRAL_REWARD_PERCENTAGE = Number(process.env.REFERRAL_REWARD_PERCENTAGE || '0.05'); +const REFERRAL_YIELD_PRECISION = 6; + +function roundToPrecision(value: number, precision = REFERRAL_YIELD_PRECISION): number { + const factor = 10 ** precision; + return Math.round(value * factor) / factor; +} + +type WalletTransactionType = 'deposit' | 'withdrawal'; + +interface WalletTransactionRecord { + amount: string; + type: WalletTransactionType; + timestamp: Date; +} + +interface SharePriceSnapshotRecord { + sharePrice: string; + recordedAt: Date; +} + +function maskWalletAddress(walletAddress: string): string { + if (walletAddress.length <= 10) { + return walletAddress; + } + + return `${walletAddress.slice(0, 4)}...${walletAddress.slice(-4)}`; +} export class ReferralService { /** @@ -18,14 +41,12 @@ export class ReferralService { const prisma = getPrisma(); try { await prisma.$transaction(async (tx) => { - // 1. If code provided, ensure relationship exists if (referralCode) { const code = await tx.referralCode.findUnique({ where: { code: referralCode }, }); if (code) { - // Check if user already has a referrer const existing = await tx.referral.findUnique({ where: { referredAddress: walletAddress }, }); @@ -45,7 +66,6 @@ export class ReferralService { } } - // 2. Check if this is the first deposit const referral = await tx.referral.findUnique({ where: { referredAddress: walletAddress }, }); @@ -65,15 +85,16 @@ export class ReferralService { error: error instanceof Error ? error.message : String(error), walletAddress, }); - // We don't throw here to avoid blocking the main deposit flow } } /** * Calculates total rewards for a referrer. - * Real-time calculation accurate to 6 decimal places. + * Rewards are computed from referred wallet net yield with 6-decimal precision. */ - async getReferralStats(referrerAddress: string): Promise<{ referral_count: number; total_reward_earned: string } | null> { + async getReferralStats( + referrerAddress: string, + ): Promise<{ referral_count: number; total_reward_earned: string } | null> { const prisma = getPrisma(); const referrals = await prisma.referral.findMany({ where: { @@ -86,53 +107,147 @@ export class ReferralService { return null; } - let totalReward = new Decimal(0); + let totalReward = 0; + let profitableReferrals = 0; for (const ref of referrals) { - const yield_earned = await this.calculateUserYield(ref.referredAddress); - if (yield_earned.gt(0)) { - const reward = yield_earned.mul(REFERRAL_REWARD_PERCENTAGE); - totalReward = totalReward.plus(reward); + const yieldEarned = await this.calculateUserYield(ref.referredAddress); + if (yieldEarned > 0) { + const reward = yieldEarned * REFERRAL_REWARD_PERCENTAGE; + totalReward += reward; + profitableReferrals += 1; } } + const roundedReward = roundToPrecision(totalReward); + + logger.log('info', 'Referral reward summary computed', { + referrer: maskWalletAddress(referrerAddress), + referralCount: referrals.length, + profitableReferrals, + totalRewardEarned: roundedReward.toFixed(REFERRAL_YIELD_PRECISION), + }); + return { referral_count: referrals.length, - total_reward_earned: totalReward.toFixed(6), + total_reward_earned: roundedReward.toFixed(REFERRAL_YIELD_PRECISION), }; } /** - * Mock implementation of yield calculation. - * In a real system, this would fetch user shares and current share price. + * Calculates user net yield from transaction history and share price snapshots. */ - private async calculateUserYield(walletAddress: string): Promise { + private async calculateUserYield(walletAddress: string): Promise { const prisma = getPrisma(); - // For the purpose of this task, we'll simulate yield. - // In a real scenario, this would be: (shares * price) - totalDeposited - // Here we'll look for transactions to at least make it dynamic-ish if they exist. - const txs = await prisma.transaction.findMany({ - where: { user: walletAddress, type: 'deposit' }, + const [transactions, snapshots] = await Promise.all([ + prisma.transaction.findMany({ + where: { + user: walletAddress, + type: { in: ['deposit', 'withdrawal'] }, + }, + orderBy: { timestamp: 'asc' }, + select: { + amount: true, + type: true, + timestamp: true, + }, + }), + prisma.sharePriceSnapshot.findMany({ + orderBy: { recordedAt: 'asc' }, + select: { + sharePrice: true, + recordedAt: true, + }, + }), + ]); + + if (transactions.length === 0 || snapshots.length === 0) { + logger.log('info', 'Referral yield calculation skipped due to missing historical data', { + wallet: maskWalletAddress(walletAddress), + transactionCount: transactions.length, + snapshotCount: snapshots.length, + }); + return 0; + } + + let shareBalance = 0; + let totalDeposited = 0; + let totalWithdrawn = 0; + let depositCount = 0; + let withdrawalCount = 0; + + for (const transaction of transactions as WalletTransactionRecord[]) { + const sharePrice = this.getSharePriceForTimestamp( + snapshots as SharePriceSnapshotRecord[], + transaction.timestamp, + ); + if (sharePrice <= 0) { + logger.log('warn', 'Referral yield calculation aborted due to invalid share price', { + wallet: maskWalletAddress(walletAddress), + transactionTimestamp: transaction.timestamp.toISOString(), + transactionType: transaction.type, + }); + return 0; + } + + const amount = Number(transaction.amount); + + if (transaction.type === 'deposit') { + depositCount += 1; + totalDeposited += amount; + shareBalance += amount / sharePrice; + } else { + withdrawalCount += 1; + totalWithdrawn += amount; + shareBalance -= amount / sharePrice; + } + } + + const latestSnapshot = snapshots[snapshots.length - 1]; + const latestSharePrice = Number(latestSnapshot.sharePrice); + const endingValue = shareBalance * latestSharePrice; + const netYield = endingValue + totalWithdrawn - totalDeposited; + + logger.log('info', 'Referral yield calculated from history', { + wallet: maskWalletAddress(walletAddress), + transactionCount: transactions.length, + depositCount, + withdrawalCount, + snapshotCount: snapshots.length, + latestSnapshotAt: latestSnapshot.recordedAt.toISOString(), + latestSharePrice: latestSharePrice.toFixed(REFERRAL_YIELD_PRECISION), + totalDeposited: totalDeposited.toFixed(REFERRAL_YIELD_PRECISION), + totalWithdrawn: totalWithdrawn.toFixed(REFERRAL_YIELD_PRECISION), + endingValue: endingValue.toFixed(REFERRAL_YIELD_PRECISION), + netYield: netYield.toFixed(REFERRAL_YIELD_PRECISION), }); - if (txs.length === 0) return new Decimal(0); + return roundToPrecision(netYield); + } + + private getSharePriceForTimestamp( + snapshots: SharePriceSnapshotRecord[], + timestamp: Date, + ): number { + let candidate = snapshots[0]; + + for (const snapshot of snapshots) { + if (snapshot.recordedAt.getTime() > timestamp.getTime()) { + break; + } + candidate = snapshot; + } - const totalDeposited = txs.reduce((sum: any, tx: any) => sum.plus(new Decimal(tx.amount)), new Decimal(0)); - - // Simulate 10% gain for demonstration purposes if there's no real price source - // Real logic would use: return currentUserValue.minus(totalDeposited).toDecimalPlaces(6); - return totalDeposited.mul('0.1').toDecimalPlaces(6); + return Number(candidate.sharePrice); } /** * Get or create a referral code for a wallet address. - * Generates a unique 8-character alphanumeric code if one doesn't exist. */ async getOrCreateReferralCode(ownerAddress: string): Promise { const prisma = getPrisma(); - // Check if code already exists - const existing = await prisma.referralCode.findUnique({ + const existing = await prisma.referralCode.findFirst({ where: { ownerAddress }, }); @@ -140,18 +255,16 @@ export class ReferralService { return existing.code; } - // Generate unique code let code: string; let attempts = 0; do { code = this.generateReferralCode(); attempts++; if (attempts > 10) { - throw new Error("Failed to generate unique referral code after 10 attempts"); + throw new Error('Failed to generate unique referral code after 10 attempts'); } } while (await prisma.referralCode.findUnique({ where: { code } })); - // Create new code await prisma.referralCode.create({ data: { code, ownerAddress }, }); @@ -159,9 +272,6 @@ export class ReferralService { return code; } - /** - * Generate a random 8-character alphanumeric referral code. - */ private generateReferralCode(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let result = ''; @@ -172,7 +282,7 @@ export class ReferralService { } /** - * Create a referral code for a wallet (helper for testing/bootstrapping). + * Create a referral code for a wallet (helper for tests). */ async createReferralCode(ownerAddress: string, code: string): Promise { const prisma = getPrisma(); diff --git a/backend/src/tracing.ts b/backend/src/tracing.ts index 0678a411..c0365888 100644 --- a/backend/src/tracing.ts +++ b/backend/src/tracing.ts @@ -1,124 +1,85 @@ /** - * OpenTelemetry distributed tracing setup. - * Must be imported BEFORE any other modules to ensure auto-instrumentation works. + * Lightweight tracing facade. * - * Configure via environment variables: - * OTEL_EXPORTER_OTLP_ENDPOINT - OTLP collector endpoint (default: http://localhost:4318) - * OTEL_SERVICE_NAME - Service name (default: yieldvault-backend) - * OTEL_ENABLED - Set to "false" to disable (default: true) + * This module intentionally avoids hard dependencies on OpenTelemetry packages + * so backend builds remain portable across CI environments. */ -import { NodeSDK } from '@opentelemetry/sdk-node'; -import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { Resource } from '@opentelemetry/resources'; -import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; -import { - trace, - context, - SpanStatusCode, - type Span, - type Tracer, -} from '@opentelemetry/api'; - -const OTEL_ENABLED = process.env.NODE_ENV !== 'test' && process.env.OTEL_ENABLED !== 'false'; -const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'yieldvault-backend'; -const OTLP_ENDPOINT = - process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318'; - -let sdk: NodeSDK | null = null; +export enum SpanStatusCode { + UNSET = 0, + OK = 1, + ERROR = 2, +} -export function initTracing(): void { - // Skip all tracing initialization in test environments or if disabled - if (!OTEL_ENABLED || IS_TEST_ENV) return; +export interface Span { + setAttributes: (attributes: Record) => void; + setStatus: (status: { code: SpanStatusCode; message?: string }) => void; + recordException: (error: Error) => void; + end: () => void; +} - // Build the instrumentations array - const instrumentations: any[] = [ - new HttpInstrumentation(), - new ExpressInstrumentation(), - ]; +export interface Tracer { + startSpan: (name: string) => Span; + startActiveSpan: (name: string, callback: (span: Span) => Promise) => Promise; +} - // Only load PrismaInstrumentation in production (non-test) environments - // The instrumentation package auto-registers hooks that can cause panics in tests - // if the Prisma Query Engine doesn't receive the expected configuration - if (!IS_TEST_ENV) { - try { - // Dynamically require to avoid loading the module at import time - // This prevents auto-instrumentation hooks from being registered prematurely - const PrismaInstrumentationModule = require('@prisma/instrumentation') as any; - if (PrismaInstrumentationModule && PrismaInstrumentationModule.PrismaInstrumentation) { - instrumentations.push(new PrismaInstrumentationModule.PrismaInstrumentation()); - } - } catch (e) { - console.warn( - 'Failed to load PrismaInstrumentation:', - e instanceof Error ? e.message : String(e), - ); - } - } +const noopSpan: Span = { + setAttributes: () => undefined, + setStatus: () => undefined, + recordException: () => undefined, + end: () => undefined, +}; - const exporter = new OTLPTraceExporter({ url: `${OTLP_ENDPOINT}/v1/traces` }); +const tracer: Tracer = { + startSpan: () => noopSpan, + startActiveSpan: async (_name: string, callback: (span: Span) => Promise) => { + return callback(noopSpan); + }, +}; - sdk = new NodeSDK({ - resource: new Resource({ - [ATTR_SERVICE_NAME]: SERVICE_NAME, - [ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0', - }), - traceExporter: exporter, - instrumentations, - }); +const OTEL_ENABLED = process.env.NODE_ENV !== 'test' && process.env.OTEL_ENABLED !== 'false'; - sdk.start(); +export function initTracing(): void { + if (!OTEL_ENABLED) { + return; + } } export async function shutdownTracing(): Promise { - if (sdk) { - await sdk.shutdown(); - } + return; } -/** Returns the active tracer for manual span creation. */ export function getTracer(): Tracer { - return trace.getTracer(SERVICE_NAME); + return tracer; } -/** - * Wraps an async function in a named span. - * Automatically records exceptions and sets error status. - */ export async function withSpan( - name: string, + _name: string, fn: (span: Span) => Promise, attributes?: Record, ): Promise { - if (!OTEL_ENABLED) return fn(trace.getTracer(SERVICE_NAME).startSpan(name)); + if (attributes) { + noopSpan.setAttributes(attributes); + } - const tracer = getTracer(); - return tracer.startActiveSpan(name, async (span) => { - if (attributes) { - span.setAttributes(attributes); - } - try { - const result = await fn(span); - span.setStatus({ code: SpanStatusCode.OK }); - return result; - } catch (err) { - span.recordException(err as Error); - span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message }); - throw err; - } finally { - span.end(); - } - }); + try { + const result = await fn(noopSpan); + noopSpan.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (err) { + noopSpan.recordException(err as Error); + noopSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: err instanceof Error ? err.message : String(err), + }); + throw err; + } finally { + noopSpan.end(); + } } -/** Returns the current trace ID for inclusion in log lines. */ export function getCurrentTraceId(): string | undefined { - const span = trace.getActiveSpan(); - if (!span) return undefined; - const ctx = span.spanContext(); - return ctx.traceId !== '00000000000000000000000000000000' ? ctx.traceId : undefined; + return undefined; } -export { context, SpanStatusCode }; +export const context = {};