diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..b947077876
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+dist/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000..a4caf2d4f8
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1825 @@
+{
+ "name": "code-challenge",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "code-challenge",
+ "version": "1.0.0",
+ "dependencies": {
+ "express": "^5.1.0"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.6",
+ "@types/node": "^24.0.0",
+ "typescript": "^5.8.0",
+ "vite": "^8.0.13"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.130.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
+ "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
+ "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
+ "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
+ "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
+ "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
+ "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
+ "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
+ "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
+ "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
+ "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
+ "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
+ "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
+ "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
+ "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.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/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": "24.12.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
+ "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "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": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
+ "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.130.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.1",
+ "@rolldown/binding-darwin-arm64": "1.0.1",
+ "@rolldown/binding-darwin-x64": "1.0.1",
+ "@rolldown/binding-freebsd-x64": "1.0.1",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.1",
+ "@rolldown/binding-linux-arm64-musl": "1.0.1",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-gnu": "1.0.1",
+ "@rolldown/binding-linux-x64-musl": "1.0.1",
+ "@rolldown/binding-openharmony-arm64": "1.0.1",
+ "@rolldown/binding-wasm32-wasi": "1.0.1",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.1",
+ "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-is": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
+ "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^2.0.0",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/type-is/node_modules/content-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+ "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "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": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.13",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
+ "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.14",
+ "rolldown": "1.0.1",
+ "tinyglobby": "^0.2.16"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.18",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..48a4ff1212
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "code-challenge",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Interview code challenge solutions",
+ "scripts": {
+ "build": "tsc",
+ "test": "npm run build && node --test dist/problem4/index.test.js dist/problem5/index.test.js",
+ "dev:problem2": "vite src/problem2 --host 0.0.0.0",
+ "build:problem2": "vite build src/problem2 --outDir ../../dist/problem2 --emptyOutDir",
+ "test:problem4": "npm run build && node --test dist/problem4/index.test.js",
+ "start:problem5": "npm run build && node dist/problem5/index.js",
+ "test:problem5": "npm run build && node --test dist/problem5/index.test.js"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.6",
+ "@types/node": "^24.0.0",
+ "typescript": "^5.8.0",
+ "vite": "^8.0.13"
+ },
+ "dependencies": {
+ "express": "^5.1.0"
+ }
+}
diff --git a/src/problem2/index.html b/src/problem2/index.html
index 4058a68bff..49276f0a9b 100644
--- a/src/problem2/index.html
+++ b/src/problem2/index.html
@@ -1,27 +1,140 @@
-
+
+
-
- Fancy Form
-
-
-
+
+
+
+ Switcheo Swap
+
+
+
+
+
+
+
+ Switcheo Exchange
+ Move between assets with a clear quote before you swap.
+ Live token pricing, instant validation, route preview, and a deliberately calm interface for confident currency conversion.
+
+
+
+
- Available pairs
+ - --
+
+
+
- Network fee
+ - 0.15%
+
+
+
- Quote source
+ - Loading
+
+
+
+
+
+
+
+
+
+
Asset Swap
+
Exchange
+
+
Fetching prices
+
+
+
+
+
-
-
-
+
diff --git a/src/problem2/script.js b/src/problem2/script.js
index e69de29bb2..b75338cc62 100644
--- a/src/problem2/script.js
+++ b/src/problem2/script.js
@@ -0,0 +1,368 @@
+const PRICE_URL = "https://interview.switcheo.com/prices.json";
+const ICON_URL = "https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens";
+const NETWORK_FEE = 0.0015;
+
+const FALLBACK_PRICES = [
+ { currency: "ETH", price: 1645.9337373737374, date: "2023-08-29T07:10:52.000Z" },
+ { currency: "WBTC", price: 26002.82202020202, date: "2023-08-29T07:10:52.000Z" },
+ { currency: "USDC", price: 1, date: "2023-08-29T07:10:30.000Z" },
+ { currency: "USD", price: 1, date: "2023-08-29T07:10:30.000Z" },
+ { currency: "ATOM", price: 7.186657333333334, date: "2023-08-29T07:10:50.000Z" },
+ { currency: "OSMO", price: 0.3772974333333333, date: "2023-08-29T07:10:50.000Z" },
+ { currency: "SWTH", price: 0.004039850455012084, date: "2023-08-29T07:10:45.000Z" },
+ { currency: "GMX", price: 36.345114372881355, date: "2023-08-29T07:10:40.000Z" },
+ { currency: "OKB", price: 42.97562059322034, date: "2023-08-29T07:10:40.000Z" },
+ { currency: "ZIL", price: 0.01651813559322034, date: "2023-08-29T07:10:50.000Z" }
+];
+
+const BALANCES = {
+ ETH: 2.4862,
+ WBTC: 0.1848,
+ USDC: 18640.42,
+ USD: 9250,
+ ATOM: 824.25,
+ OSMO: 4200,
+ SWTH: 188000,
+ GMX: 34.8,
+ OKB: 118.45,
+ ZIL: 92000
+};
+
+const state = {
+ tokens: [],
+ from: "ETH",
+ to: "USDC",
+ amount: "",
+ pickerTarget: null,
+ isSubmitting: false
+};
+
+const els = {
+ form: document.querySelector("#swap-form"),
+ fromAmount: document.querySelector("#from-amount"),
+ toAmount: document.querySelector("#to-amount"),
+ fromSymbol: document.querySelector("#from-symbol"),
+ toSymbol: document.querySelector("#to-symbol"),
+ fromIcon: document.querySelector("#from-icon"),
+ toIcon: document.querySelector("#to-icon"),
+ fromBalance: document.querySelector("#from-balance"),
+ toBalance: document.querySelector("#to-balance"),
+ fromValue: document.querySelector("#from-value"),
+ toValue: document.querySelector("#to-value"),
+ formError: document.querySelector("#form-error"),
+ rateLine: document.querySelector("#rate-line"),
+ minimumLine: document.querySelector("#minimum-line"),
+ routeLine: document.querySelector("#route-line"),
+ slippage: document.querySelector("#slippage"),
+ submitButton: document.querySelector("#submit-button"),
+ switchButton: document.querySelector("#switch-button"),
+ maxButton: document.querySelector("#max-button"),
+ popover: document.querySelector("#token-popover"),
+ tokenList: document.querySelector("#token-list"),
+ tokenSearch: document.querySelector("#token-search"),
+ closePicker: document.querySelector("#close-picker"),
+ toast: document.querySelector("#toast"),
+ pairCount: document.querySelector("#pair-count"),
+ priceSource: document.querySelector("#price-source"),
+ priceStatus: document.querySelector("#price-status"),
+ assetStrip: document.querySelector("#asset-strip")
+};
+
+init();
+
+async function init() {
+ bindEvents();
+ setLoadingState(true);
+
+ try {
+ const response = await fetch(PRICE_URL, { cache: "no-store" });
+ if (!response.ok) throw new Error("Price request failed");
+ const prices = await response.json();
+ state.tokens = normalizePrices(prices);
+ els.priceSource.textContent = "Live";
+ els.priceStatus.textContent = "Live prices";
+ } catch (error) {
+ state.tokens = normalizePrices(FALLBACK_PRICES);
+ els.priceSource.textContent = "Fallback";
+ els.priceStatus.textContent = "Offline prices";
+ } finally {
+ setLoadingState(false);
+ els.priceStatus.classList.add("ready");
+ ensureSelectedTokens();
+ renderAll();
+ }
+}
+
+function bindEvents() {
+ els.fromAmount.addEventListener("input", (event) => {
+ state.amount = sanitizeAmount(event.target.value);
+ event.target.value = state.amount;
+ renderQuote();
+ });
+
+ els.slippage.addEventListener("change", renderQuote);
+ els.switchButton.addEventListener("click", switchTokens);
+ els.maxButton.addEventListener("click", useMaxBalance);
+ els.closePicker.addEventListener("click", closePicker);
+ els.popover.addEventListener("click", (event) => {
+ if (event.target === els.popover) closePicker();
+ });
+ els.tokenSearch.addEventListener("input", renderTokenOptions);
+
+ document.querySelectorAll("[data-picker]").forEach((button) => {
+ button.addEventListener("click", () => openPicker(button.dataset.picker));
+ });
+
+ els.form.addEventListener("submit", submitSwap);
+}
+
+function normalizePrices(prices) {
+ const byCurrency = new Map();
+
+ prices.forEach((item) => {
+ if (!item.currency || !Number.isFinite(Number(item.price)) || Number(item.price) <= 0) return;
+
+ const current = byCurrency.get(item.currency);
+ const nextDate = new Date(item.date || 0).getTime();
+ const currentDate = current ? new Date(current.date || 0).getTime() : -1;
+
+ if (!current || nextDate >= currentDate) {
+ byCurrency.set(item.currency, {
+ symbol: item.currency,
+ price: Number(item.price),
+ date: item.date || "",
+ balance: BALANCES[item.currency] ?? makeBalance(item.currency)
+ });
+ }
+ });
+
+ return [...byCurrency.values()].sort((a, b) => b.price - a.price);
+}
+
+function makeBalance(symbol) {
+ const seed = [...symbol].reduce((sum, char) => sum + char.charCodeAt(0), 0);
+ return Number(((seed % 1800) + 25 + (seed % 19) / 100).toFixed(4));
+}
+
+function ensureSelectedTokens() {
+ if (!findToken(state.from)) state.from = state.tokens[0]?.symbol;
+ if (!findToken(state.to) || state.to === state.from) {
+ state.to = state.tokens.find((token) => token.symbol !== state.from)?.symbol;
+ }
+}
+
+function renderAll() {
+ const count = state.tokens.length;
+ els.pairCount.textContent = count > 1 ? `${count * (count - 1)}` : "--";
+ renderFeaturedAssets();
+ renderTokenSelectors();
+ renderQuote();
+}
+
+function renderFeaturedAssets() {
+ els.assetStrip.innerHTML = state.tokens
+ .slice(0, 8)
+ .map((token) => `
+
+
+ ${token.symbol} - ${formatUsd(token.price)}
+
+ `)
+ .join("");
+}
+
+function renderTokenSelectors() {
+ const from = findToken(state.from);
+ const to = findToken(state.to);
+ if (!from || !to) return;
+
+ els.fromSymbol.textContent = from.symbol;
+ els.toSymbol.textContent = to.symbol;
+ els.fromIcon.src = iconFor(from.symbol);
+ els.toIcon.src = iconFor(to.symbol);
+ els.fromIcon.onerror = () => hideBrokenImage(els.fromIcon);
+ els.toIcon.onerror = () => hideBrokenImage(els.toIcon);
+ els.fromBalance.textContent = `Balance ${formatToken(from.balance, from.symbol)}`;
+ els.toBalance.textContent = `Balance ${formatToken(to.balance, to.symbol)}`;
+}
+
+function renderQuote() {
+ const from = findToken(state.from);
+ const to = findToken(state.to);
+ const amount = Number(state.amount);
+ const validation = validateAmount(amount, from, to);
+
+ if (!from || !to) return;
+
+ const grossReceive = amount > 0 ? amount * from.price / to.price : 0;
+ const receive = grossReceive * (1 - NETWORK_FEE);
+ const slippage = Number(els.slippage.value) / 100;
+ const minimum = receive * (1 - slippage);
+
+ els.toAmount.value = receive ? trimNumber(receive) : "";
+ els.fromValue.textContent = formatUsd(amount > 0 ? amount * from.price : 0);
+ els.toValue.textContent = formatUsd(receive * to.price);
+ els.rateLine.textContent = `1 ${from.symbol} = ${trimNumber(from.price / to.price)} ${to.symbol}`;
+ els.minimumLine.textContent = receive ? `${trimNumber(minimum)} ${to.symbol}` : "--";
+ els.routeLine.textContent = `${from.symbol} -> USD -> ${to.symbol}`;
+ els.formError.textContent = validation.message;
+ els.submitButton.disabled = Boolean(validation.message) || state.isSubmitting;
+ els.submitButton.textContent = state.isSubmitting ? "Confirming swap..." : "Review swap";
+}
+
+function validateAmount(amount, from, to) {
+ if (!from || !to) return { message: "Token prices are still loading." };
+ if (from.symbol === to.symbol) return { message: "Choose two different currencies." };
+ if (!state.amount) return { message: "Enter an amount to preview your swap." };
+ if (!Number.isFinite(amount) || amount <= 0) return { message: "Amount must be greater than zero." };
+ if (amount > from.balance) return { message: `Insufficient ${from.symbol} balance.` };
+ return { message: "" };
+}
+
+function openPicker(target) {
+ state.pickerTarget = target;
+ els.tokenSearch.value = "";
+ els.popover.hidden = false;
+ document.querySelector(`[data-picker="${target}"]`).setAttribute("aria-expanded", "true");
+ renderTokenOptions();
+ window.setTimeout(() => els.tokenSearch.focus(), 0);
+}
+
+function closePicker() {
+ document.querySelectorAll("[data-picker]").forEach((button) => button.setAttribute("aria-expanded", "false"));
+ state.pickerTarget = null;
+ els.popover.hidden = true;
+}
+
+function renderTokenOptions() {
+ const query = els.tokenSearch.value.trim().toLowerCase();
+ const activeSymbol = state[state.pickerTarget] || "";
+
+ const options = state.tokens
+ .filter((token) => token.symbol.toLowerCase().includes(query))
+ .map((token) => `
+
+ `)
+ .join("");
+
+ els.tokenList.innerHTML = options || `No matching token found.
`;
+ els.tokenList.querySelectorAll("[data-symbol]").forEach((button) => {
+ button.addEventListener("click", () => selectToken(button.dataset.symbol));
+ });
+}
+
+function selectToken(symbol) {
+ const otherSide = state.pickerTarget === "from" ? "to" : "from";
+ if (symbol === state[otherSide]) {
+ state[otherSide] = state[state.pickerTarget];
+ }
+ state[state.pickerTarget] = symbol;
+ closePicker();
+ renderTokenSelectors();
+ renderQuote();
+}
+
+function switchTokens() {
+ [state.from, state.to] = [state.to, state.from];
+ renderTokenSelectors();
+ renderQuote();
+}
+
+function useMaxBalance() {
+ const from = findToken(state.from);
+ state.amount = trimNumber(from?.balance || 0);
+ els.fromAmount.value = state.amount;
+ renderQuote();
+}
+
+async function submitSwap(event) {
+ event.preventDefault();
+ const from = findToken(state.from);
+ const to = findToken(state.to);
+ const amount = Number(state.amount);
+ const validation = validateAmount(amount, from, to);
+ if (validation.message) {
+ els.formError.textContent = validation.message;
+ return;
+ }
+
+ state.isSubmitting = true;
+ renderQuote();
+ await delay(1100);
+
+ const received = Number(els.toAmount.value);
+ from.balance = Number((from.balance - amount).toFixed(8));
+ to.balance = Number((to.balance + received).toFixed(8));
+ state.amount = "";
+ els.fromAmount.value = "";
+ state.isSubmitting = false;
+
+ renderTokenSelectors();
+ renderQuote();
+ showToast(`Swap confirmed: ${formatToken(amount, from.symbol)} for ${formatToken(received, to.symbol)}.`);
+}
+
+function setLoadingState(isLoading) {
+ els.submitButton.disabled = isLoading;
+ els.submitButton.textContent = isLoading ? "Loading market..." : "Review swap";
+}
+
+function findToken(symbol) {
+ return state.tokens.find((token) => token.symbol === symbol);
+}
+
+function iconFor(symbol) {
+ return `${ICON_URL}/${encodeURIComponent(symbol)}.svg`;
+}
+
+function hideBrokenImage(image) {
+ image.style.visibility = "hidden";
+}
+
+function sanitizeAmount(value) {
+ const cleaned = value.replace(/[^0-9.]/g, "");
+ const [whole, ...rest] = cleaned.split(".");
+ const decimal = rest.join("").slice(0, 8);
+ return rest.length ? `${whole || "0"}.${decimal}` : whole;
+}
+
+function trimNumber(value) {
+ if (!Number.isFinite(Number(value))) return "";
+ return Number(value).toLocaleString("en-US", {
+ maximumFractionDigits: Number(value) >= 1 ? 6 : 8,
+ useGrouping: false
+ });
+}
+
+function formatToken(value, symbol) {
+ return `${trimNumber(value)} ${symbol}`;
+}
+
+function formatUsd(value) {
+ return Number(value || 0).toLocaleString("en-US", {
+ style: "currency",
+ currency: "USD",
+ maximumFractionDigits: value >= 1 ? 2 : 6
+ });
+}
+
+function formatCompact(value) {
+ return Number(value || 0).toLocaleString("en-US", {
+ notation: "compact",
+ maximumFractionDigits: 2
+ });
+}
+
+function delay(ms) {
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
+}
+
+function showToast(message) {
+ els.toast.textContent = message;
+ els.toast.classList.add("visible");
+ window.setTimeout(() => els.toast.classList.remove("visible"), 3600);
+}
diff --git a/src/problem2/style.css b/src/problem2/style.css
index 915af91c72..7ad41a0659 100644
--- a/src/problem2/style.css
+++ b/src/problem2/style.css
@@ -1,8 +1,527 @@
+* {
+ box-sizing: border-box;
+}
+
+:root {
+ color-scheme: dark;
+ --bg: #101113;
+ --panel: #181b20;
+ --panel-strong: #20242b;
+ --line: rgba(255, 255, 255, 0.11);
+ --text: #f5f2ec;
+ --muted: #a7abb3;
+ --soft: #d4d7de;
+ --accent: #45d19f;
+ --accent-2: #ffbc57;
+ --danger: #ff7373;
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
+}
+
body {
+ min-width: 360px;
+ min-height: 100vh;
+ margin: 0;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ color: var(--text);
+ background:
+ radial-gradient(circle at 16% 12%, rgba(69, 209, 159, 0.15), transparent 26rem),
+ radial-gradient(circle at 86% 18%, rgba(255, 188, 87, 0.12), transparent 24rem),
+ linear-gradient(145deg, #111315 0%, #16191e 48%, #101113 100%);
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+button {
+ border: 0;
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: not-allowed;
+}
+
+.app-shell {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(390px, 480px);
+ gap: 44px;
+ width: min(1160px, calc(100% - 40px));
+ min-height: 100vh;
+ margin: 0 auto;
+ align-items: center;
+ padding: 44px 0;
+}
+
+.market-panel {
+ min-width: 0;
+}
+
+.brand-mark {
+ display: grid;
+ place-items: center;
+ width: 64px;
+ height: 64px;
+ margin-bottom: 28px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ border-radius: 18px;
+ background: linear-gradient(135deg, rgba(69, 209, 159, 0.22), rgba(255, 188, 87, 0.18));
+ box-shadow: inset 0 1px rgba(255, 255, 255, 0.18), 0 20px 60px rgba(0, 0, 0, 0.22);
+}
+
+.brand-mark span {
+ width: 28px;
+ height: 28px;
+ border-radius: 999px;
+ background: conic-gradient(from 70deg, var(--accent), var(--accent-2), #fa7070, var(--accent));
+ box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.07);
+}
+
+.eyebrow {
+ margin: 0 0 10px;
+ color: var(--accent);
+ font-size: 0.78rem;
+ font-weight: 800;
+ letter-spacing: 0;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1 {
+ max-width: 720px;
+ margin-bottom: 18px;
+ font-size: clamp(2.7rem, 5vw, 5.8rem);
+ line-height: 0.95;
+ letter-spacing: 0;
+}
+
+.hero-copy {
+ max-width: 620px;
+ margin-bottom: 34px;
+ color: var(--soft);
+ font-size: 1.08rem;
+ line-height: 1.7;
+}
+
+.market-stats {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+ max-width: 680px;
+ margin: 0 0 28px;
+}
+
+.market-stats div,
+.asset-chip {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.045);
+}
+
+.market-stats div {
+ padding: 16px;
+}
+
+dt {
+ color: var(--muted);
+ font-size: 0.78rem;
+}
+
+dd {
+ margin: 6px 0 0;
+ font-size: 1.2rem;
+ font-weight: 800;
+}
+
+.asset-strip {
display: flex;
- flex-direction: row;
+ flex-wrap: wrap;
+ gap: 10px;
+ max-width: 680px;
+}
+
+.asset-chip {
+ display: inline-flex;
align-items: center;
- justify-content: center;
- min-width: 360px;
- font-family: Arial, Helvetica, sans-serif;
+ gap: 8px;
+ padding: 9px 11px;
+ color: var(--soft);
+ font-size: 0.88rem;
+}
+
+.asset-chip img,
+.token-button img,
+.token-option img {
+ width: 26px;
+ height: 26px;
+ border-radius: 999px;
+ background: #2a2f37;
+}
+
+.swap-card {
+ position: relative;
+ border: 1px solid rgba(255, 255, 255, 0.13);
+ border-radius: 8px;
+ padding: 24px;
+ background: linear-gradient(180deg, rgba(32, 36, 43, 0.94), rgba(24, 27, 32, 0.94));
+ box-shadow: var(--shadow);
+}
+
+.card-heading,
+.field-row,
+.token-row,
+.settings-row,
+.popover-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.card-heading {
+ margin-bottom: 20px;
+}
+
+.card-heading h2 {
+ margin: 0;
+ font-size: 2rem;
+}
+
+.status-pill {
+ padding: 8px 10px;
+ border-radius: 999px;
+ color: var(--accent-2);
+ background: rgba(255, 188, 87, 0.12);
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.status-pill.ready {
+ color: var(--accent);
+ background: rgba(69, 209, 159, 0.12);
+}
+
+.amount-box {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.field-row {
+ min-height: 24px;
+ color: var(--muted);
+ font-size: 0.86rem;
+}
+
+.field-row label {
+ color: var(--soft);
+ font-weight: 750;
+}
+
+.subtle {
+ margin-top: 8px;
+}
+
+.token-row {
+ margin-top: 10px;
+}
+
+.token-row input {
+ min-width: 0;
+ width: 100%;
+ border: 0;
+ outline: 0;
+ color: var(--text);
+ background: transparent;
+ font-size: clamp(1.8rem, 5vw, 2.65rem);
+ font-weight: 800;
+ letter-spacing: 0;
+}
+
+.token-row input::placeholder {
+ color: rgba(255, 255, 255, 0.24);
+}
+
+.token-button {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 0 auto;
+ gap: 9px;
+ min-width: 126px;
+ padding: 10px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 8px;
+ color: var(--text);
+ background: #101317;
+ font-weight: 850;
+}
+
+.chevron {
+ margin-left: auto;
+ color: var(--muted);
+}
+
+.ghost-button {
+ padding: 4px 8px;
+ border-radius: 7px;
+ color: var(--accent);
+ background: rgba(69, 209, 159, 0.12);
+ font-weight: 800;
+}
+
+.switch-button {
+ display: grid;
+ place-items: center;
+ width: 42px;
+ height: 42px;
+ margin: -2px auto;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ color: var(--text);
+ background: linear-gradient(135deg, #2a3038, #111418);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+ transform: translateY(0);
+ transition: transform 160ms ease, border-color 160ms ease;
+}
+
+.switch-button:hover {
+ border-color: rgba(69, 209, 159, 0.55);
+ transform: translateY(-1px);
+}
+
+.form-error {
+ min-height: 22px;
+ margin: 12px 0;
+ color: var(--danger);
+ font-size: 0.9rem;
+}
+
+.quote-panel {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 16px;
+ padding: 14px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.quote-panel div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ color: var(--muted);
+ font-size: 0.86rem;
+}
+
+.quote-panel strong {
+ color: var(--soft);
+ text-align: right;
+}
+
+.settings-row {
+ margin-bottom: 18px;
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.settings-row select {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 9px 12px;
+ color: var(--text);
+ background: #101317;
+}
+
+.submit-button {
+ width: 100%;
+ min-height: 54px;
+ border-radius: 8px;
+ color: #07110d;
+ background: linear-gradient(135deg, var(--accent), #9be47a);
+ font-size: 1rem;
+ font-weight: 900;
+ box-shadow: 0 18px 42px rgba(69, 209, 159, 0.25);
+}
+
+.submit-button:disabled {
+ opacity: 0.55;
+ box-shadow: none;
+}
+
+.token-popover {
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ display: grid;
+ place-items: center;
+ padding: 20px;
+ background: rgba(0, 0, 0, 0.58);
+ backdrop-filter: blur(10px);
+}
+
+.token-popover[hidden] {
+ display: none;
+}
+
+.popover-panel {
+ width: min(430px, 100%);
+ max-height: min(680px, calc(100vh - 40px));
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: #171b21;
+ box-shadow: var(--shadow);
+}
+
+.popover-header {
+ padding: 18px 18px 8px;
+}
+
+.popover-header h3 {
+ margin: 0;
+}
+
+.popover-header button {
+ width: 34px;
+ height: 34px;
+ border-radius: 8px;
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.08);
+ font-size: 1.4rem;
+}
+
+.token-search {
+ width: calc(100% - 36px);
+ margin: 10px 18px 12px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 12px 13px;
+ outline: 0;
+ color: var(--text);
+ background: #101317;
+}
+
+.token-list {
+ max-height: 430px;
+ overflow: auto;
+ padding: 6px 10px 12px;
+}
+
+.token-option {
+ display: grid;
+ grid-template-columns: 34px minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ padding: 11px 10px;
+ border-radius: 8px;
+ color: var(--text);
+ background: transparent;
+ text-align: left;
+}
+
+.token-option:hover,
+.token-option[aria-selected="true"] {
+ background: rgba(255, 255, 255, 0.07);
+}
+
+.token-option strong,
+.token-option span {
+ display: block;
+}
+
+.token-option span {
+ margin-top: 2px;
+ color: var(--muted);
+ font-size: 0.82rem;
+}
+
+.token-option em {
+ color: var(--soft);
+ font-style: normal;
+ font-weight: 800;
+}
+
+.toast {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ z-index: 30;
+ max-width: min(420px, calc(100% - 40px));
+ padding: 14px 16px;
+ border: 1px solid rgba(69, 209, 159, 0.35);
+ border-radius: 8px;
+ color: var(--text);
+ background: #15221d;
+ box-shadow: var(--shadow);
+ opacity: 0;
+ transform: translateY(12px);
+ pointer-events: none;
+ transition: opacity 180ms ease, transform 180ms ease;
+}
+
+.toast.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+@media (max-width: 900px) {
+ .app-shell {
+ grid-template-columns: 1fr;
+ gap: 28px;
+ align-items: start;
+ padding-top: 28px;
+ }
+
+ h1 {
+ font-size: clamp(2.35rem, 12vw, 4rem);
+ }
+
+ .hero-copy {
+ margin-bottom: 22px;
+ }
+}
+
+@media (max-width: 560px) {
+ .app-shell {
+ width: min(100% - 24px, 1160px);
+ }
+
+ .market-stats {
+ grid-template-columns: 1fr;
+ }
+
+ .swap-card {
+ padding: 18px;
+ }
+
+ .token-row {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .token-button {
+ width: 100%;
+ }
+
+ .quote-panel div,
+ .settings-row {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .quote-panel strong {
+ text-align: left;
+ }
}
diff --git a/src/problem3/README.md b/src/problem3/README.md
new file mode 100644
index 0000000000..587b212fac
--- /dev/null
+++ b/src/problem3/README.md
@@ -0,0 +1,164 @@
+# Problem 3 - Messy React
+
+The original component mixes correctness issues, avoidable recomputation, weak
+typing, and React anti-patterns. The largest problems are not stylistic; several
+of them change the rendered output or make the code fail TypeScript compilation.
+
+## Issues Found
+
+### Type and compile-time correctness
+
+- `WalletBalance` does not define `blockchain`, but the component reads
+ `balance.blockchain`. This should be part of the domain type.
+- `getPriority` accepts `any`, which removes TypeScript's protection around a
+ known blockchain value.
+- The filter references `lhsPriority`, which is not defined. The intended
+ variable appears to be `balancePriority`.
+- `rows` maps over `sortedBalances` but types each item as
+ `FormattedWalletBalance`. At runtime, `sortedBalances` does not include the
+ `formatted` field.
+- The `sort` comparator can return `undefined` when priorities are equal. A
+ comparator must return a number for every branch.
+- `Props extends BoxProps` is empty and the component renders a native `div`.
+ Unless `BoxProps` is intentionally supported by that `div` or a `Box`
+ component, this is an inaccurate public contract.
+- `classes.row` is used but `classes` is not defined in the snippet.
+
+### Business logic bugs
+
+- The filter condition is inverted. It currently keeps balances with
+ `amount <= 0` when the priority is valid. A wallet page should normally show
+ positive balances and hide zero or negative balances.
+- The default priority is `-99`, but the broken filter condition makes the
+ eligibility rule hard to read and easy to misuse.
+- `formattedBalances` is calculated but never used for rendering.
+- `balance.amount.toFixed()` defaults to zero decimal places, which is usually
+ too lossy for wallet/token balances. Formatting should be explicit.
+- `prices[balance.currency] * balance.amount` can produce `NaN` when a price is
+ missing.
+
+### React and performance anti-patterns
+
+- `getPriority` is recreated on every render and is listed indirectly inside a
+ memoized calculation. A static lookup table is simpler and more stable.
+- `prices` is included in the `sortedBalances` dependency array even though
+ sorting/filtering does not use prices. That causes unnecessary recomputation
+ whenever prices change.
+- The code performs multiple passes over the same data:
+ `filter -> sort -> map formattedBalances -> map rows`. The unused
+ `formattedBalances` pass is pure waste, and the final row data can be prepared
+ in one memoized transformation after sorting.
+- `Array.prototype.sort` mutates its input. In this specific code the array
+ returned by `filter` is new, so `balances` is not mutated, but relying on this
+ detail is fragile. Copying before sorting communicates the intent clearly.
+- Using `index` as the React `key` can cause incorrect row identity when the
+ list is sorted or changes. A stable key from the data should be used.
+- The component accepts `children` but never renders it. Either render children
+ intentionally or remove it from the destructuring.
+- Rendering rows can be memoized as derived row data first, keeping JSX mapping
+ simple and avoiding stale or mismatched derived values.
+
+## Refactored Version
+
+```tsx
+interface WalletBalance {
+ currency: string;
+ amount: number;
+ blockchain: Blockchain;
+}
+
+interface FormattedWalletBalance extends WalletBalance {
+ formatted: string;
+ usdValue: number;
+}
+
+type Blockchain = 'Osmosis' | 'Ethereum' | 'Arbitrum' | 'Zilliqa' | 'Neo';
+
+interface Props extends BoxProps {
+ children?: React.ReactNode;
+}
+
+const BLOCKCHAIN_PRIORITY: Record = {
+ Osmosis: 100,
+ Ethereum: 50,
+ Arbitrum: 30,
+ Zilliqa: 20,
+ Neo: 20,
+};
+
+const UNKNOWN_BLOCKCHAIN_PRIORITY = -99;
+
+const getPriority = (blockchain: string): number => {
+ return BLOCKCHAIN_PRIORITY[blockchain as Blockchain] ?? UNKNOWN_BLOCKCHAIN_PRIORITY;
+};
+
+const formatTokenAmount = (amount: number): string => {
+ return amount.toFixed(2);
+};
+
+const WalletPage: React.FC = ({ children, ...rest }) => {
+ const balances = useWalletBalances();
+ const prices = usePrices();
+
+ const rows = useMemo(() => {
+ return [...balances]
+ .filter((balance) => {
+ return getPriority(balance.blockchain) > UNKNOWN_BLOCKCHAIN_PRIORITY
+ && balance.amount > 0;
+ })
+ .sort((left, right) => {
+ return getPriority(right.blockchain) - getPriority(left.blockchain);
+ })
+ .map((balance) => {
+ const price = prices[balance.currency] ?? 0;
+
+ return {
+ ...balance,
+ formatted: formatTokenAmount(balance.amount),
+ usdValue: price * balance.amount,
+ };
+ });
+ }, [balances, prices]);
+
+ return (
+
+ {children}
+ {rows.map((balance) => (
+
+ ))}
+
+ );
+};
+```
+
+## Why This Is Better
+
+- The transformation now has one clear data flow: validate balances, sort by
+ priority, then derive display values.
+- TypeScript describes the real data shape instead of hiding missing fields with
+ `any`.
+- Filtering now keeps only supported blockchains with positive balances.
+- The sort comparator is deterministic and always returns a number.
+- Price and formatted amount are calculated in the same memoized transformation
+ that produces the renderable row data.
+- React keys are stable across sorting and insertion.
+- Missing prices produce a safe zero USD value instead of `NaN`.
+
+## Further Production Improvements
+
+- Replace `balance.amount` as `number` with a decimal-safe representation if
+ token precision matters. JavaScript floating-point numbers are not ideal for
+ financial calculations.
+- Use `Intl.NumberFormat` or a token-aware formatter for display formatting
+ instead of a hardcoded `toFixed(2)`.
+- If unsupported blockchains should still render with low priority, keep them
+ with a default priority instead of filtering them out.
+- Prefer a concrete container prop type, such as
+ `React.ComponentPropsWithoutRef<'div'>`, if the component renders a `div`
+ rather than a design-system `Box`.
diff --git a/src/problem4/index.test.ts b/src/problem4/index.test.ts
new file mode 100644
index 0000000000..72bacb5272
--- /dev/null
+++ b/src/problem4/index.test.ts
@@ -0,0 +1,24 @@
+import { strictEqual } from "node:assert";
+import { test } from "node:test";
+
+import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "./index";
+
+const implementations = [
+ ["sum_to_n_a", sum_to_n_a],
+ ["sum_to_n_b", sum_to_n_b],
+ ["sum_to_n_c", sum_to_n_c],
+] as const;
+
+for (const [name, sumToN] of implementations) {
+ test(`${name} returns the summation from 1 to n`, () => {
+ strictEqual(sumToN(1), 1);
+ strictEqual(sumToN(5), 15);
+ strictEqual(sumToN(10), 55);
+ strictEqual(sumToN(100), 5050);
+ });
+
+ test(`${name} returns 0 for non-positive input`, () => {
+ strictEqual(sumToN(0), 0);
+ strictEqual(sumToN(-5), 0);
+ });
+}
diff --git a/src/problem4/index.ts b/src/problem4/index.ts
new file mode 100644
index 0000000000..e7bcb1c6f3
--- /dev/null
+++ b/src/problem4/index.ts
@@ -0,0 +1,79 @@
+/**
+ * @file Problem 4 - Summation to N
+ * @description Provides three unique TypeScript implementations for calculating
+ * the summation from 1 to n.
+ * @author Paing Sett Kyaw
+ * @created 2026-05-15
+ * @updated 2026-05-15
+ */
+
+/**
+ * @description Calculates the summation from 1 to n using a standard iterative
+ * loop.
+ * @param n - Any integer input.
+ * @returns The summation from 1 to n. Returns 0 when n is less than or equal to 0.
+ * @complexity Time: O(n), because each number from 1 through n is visited once.
+ * Space: O(1), because only a running total is stored.
+ */
+function sum_to_n_a(n: number): number {
+ if (n <= 0) {
+ return 0;
+ }
+
+ let total = 0;
+
+ for (let current = 1; current <= n; current += 1) {
+ total += current;
+ }
+
+ return total;
+}
+
+/**
+ * @description Calculates the summation from 1 to n using the arithmetic series
+ * formula.
+ * @param n - Any integer input.
+ * @returns The summation from 1 to n. Returns 0 when n is less than or equal to 0.
+ * @complexity Time: O(1), because the result is calculated directly with a
+ * formula. Space: O(1), because no extra data structure is created.
+ */
+function sum_to_n_b(n: number): number {
+ if (n <= 0) {
+ return 0;
+ }
+
+ return (n * (n + 1)) / 2;
+}
+
+/**
+ * @description Calculates the summation from 1 to n by pairing the smallest and
+ * largest remaining values.
+ * @param n - Any integer input.
+ * @returns The summation from 1 to n. Returns 0 when n is less than or equal to 0.
+ * @complexity Time: O(n), but it performs about half as many loop iterations as
+ * the direct iterative solution. Space: O(1), because only counters and a
+ * running total are stored.
+ */
+function sum_to_n_c(n: number): number {
+ if (n <= 0) {
+ return 0;
+ }
+
+ let low = 1;
+ let high = n;
+ let total = 0;
+
+ while (low < high) {
+ total += low + high;
+ low += 1;
+ high -= 1;
+ }
+
+ if (low === high) {
+ total += low;
+ }
+
+ return total;
+}
+
+export { sum_to_n_a, sum_to_n_b, sum_to_n_c };
diff --git a/src/problem5/README.md b/src/problem5/README.md
new file mode 100644
index 0000000000..d7b303d213
--- /dev/null
+++ b/src/problem5/README.md
@@ -0,0 +1,175 @@
+# Problem 5 - A Crude Server
+
+This solution implements a TypeScript ExpressJS backend with CRUD endpoints for
+a `resource` entity. Data is persisted in a small JSON database file so records
+remain available after the server restarts.
+
+## Requirements Covered
+
+- Create a resource.
+- List resources with basic filters.
+- Get details of one resource.
+- Update resource details.
+- Delete a resource.
+- Persist data through a simple file-backed database.
+
+## Configuration
+
+The server supports these environment variables:
+
+| Variable | Default | Description |
+| --- | --- | --- |
+| `PORT` | `3000` | HTTP port used by the Express server. |
+| `DATABASE_FILE` | `dist/problem5/database.json` | JSON database file path. |
+
+## Run The Application
+
+Install dependencies:
+
+```bash
+npm install
+```
+
+Build and start Problem 5:
+
+```bash
+npm run start:problem5
+```
+
+The server will run at:
+
+```text
+http://localhost:3000
+```
+
+Use a custom port or database file:
+
+```bash
+PORT=4000 DATABASE_FILE=./problem5-db.json npm run start:problem5
+```
+
+## Project Structure
+
+```text
+src/problem5/
+ index.ts # Server entrypoint and public exports
+ app.ts # Express app and route handlers
+ json-resource-database.ts # File-backed resource repository
+ validation.ts # Request and database validation helpers
+ errors.ts # HTTP error and centralized error middleware
+ types.ts # Shared resource and repository types
+ constants.ts # Shared constants
+ index.test.ts # CRUD and validation tests
+```
+
+## API Endpoints
+
+### API Home
+
+```http
+GET /
+```
+
+Shows a simple browser-friendly page confirming the API is running and linking
+to useful endpoints.
+
+### Health Check
+
+```http
+GET /health
+```
+
+### Create Resource
+
+```http
+POST /resources
+Content-Type: application/json
+```
+
+```json
+{
+ "title": "Interview preparation notes",
+ "description": "Backend CRUD practice task",
+ "category": "study",
+ "status": "active",
+ "tags": ["typescript", "express"]
+}
+```
+
+Required fields:
+
+- `title`
+- `category`
+
+Optional fields:
+
+- `description`
+- `status`: `active` or `archived`
+- `tags`: array of strings
+
+### List Resources
+
+```http
+GET /resources
+```
+
+Basic filters:
+
+```http
+GET /resources?category=study
+GET /resources?status=active
+GET /resources?tag=typescript
+GET /resources?search=backend
+```
+
+Filters can be combined:
+
+```http
+GET /resources?category=study&status=active&tag=typescript
+```
+
+### Get Resource Details
+
+```http
+GET /resources/:id
+```
+
+### Update Resource
+
+```http
+PATCH /resources/:id
+Content-Type: application/json
+```
+
+```json
+{
+ "title": "Updated interview notes",
+ "status": "archived"
+}
+```
+
+### Delete Resource
+
+```http
+DELETE /resources/:id
+```
+
+Successful deletion returns `204 No Content`.
+
+## Example Curl Flow
+
+```bash
+curl -X POST http://localhost:3000/resources \
+ -H "Content-Type: application/json" \
+ -d '{"title":"Interview preparation notes","category":"study","tags":["typescript","express"]}'
+
+curl http://localhost:3000/resources
+
+curl "http://localhost:3000/resources?category=study&tag=typescript"
+
+curl -X PATCH http://localhost:3000/resources/ \
+ -H "Content-Type: application/json" \
+ -d '{"status":"archived"}'
+
+curl -X DELETE http://localhost:3000/resources/
+```
diff --git a/src/problem5/app.ts b/src/problem5/app.ts
new file mode 100644
index 0000000000..11340db8d0
--- /dev/null
+++ b/src/problem5/app.ts
@@ -0,0 +1,140 @@
+import express, { type Request, type RequestHandler, type Response } from "express";
+import { HttpError, errorHandler } from "./errors";
+import { JsonResourceDatabase } from "./json-resource-database";
+import { type ResourceRepository } from "./types";
+import {
+ parseCreateResourceInput,
+ parseResourceFilters,
+ parseUpdateResourceInput,
+ readRouteId,
+} from "./validation";
+
+/**
+ * @description Creates the Express application and registers all Problem 5 routes.
+ * @param repository - Resource persistence implementation used by route handlers.
+ * @returns Configured Express application instance.
+ */
+export function createApp(
+ repository: ResourceRepository = new JsonResourceDatabase(),
+): express.Express {
+ const app = express();
+
+ app.use(express.json());
+
+ app.get("/", renderHomePage);
+ app.get("/health", healthCheck);
+
+ app.get(
+ "/resources",
+ asyncRoute(async (request, response) => {
+ const resources = await repository.list(parseResourceFilters(request));
+
+ response.json({ data: resources, count: resources.length });
+ }),
+ );
+
+ app.post(
+ "/resources",
+ asyncRoute(async (request, response) => {
+ const resource = await repository.create(parseCreateResourceInput(request.body));
+
+ response.status(201).json({ data: resource });
+ }),
+ );
+
+ app.get(
+ "/resources/:id",
+ asyncRoute(async (request, response) => {
+ const resource = await repository.findById(readRouteId(request));
+
+ if (!resource) {
+ throw new HttpError(404, "Resource not found");
+ }
+
+ response.json({ data: resource });
+ }),
+ );
+
+ app.patch(
+ "/resources/:id",
+ asyncRoute(async (request, response) => {
+ const resource = await repository.update(
+ readRouteId(request),
+ parseUpdateResourceInput(request.body),
+ );
+
+ if (!resource) {
+ throw new HttpError(404, "Resource not found");
+ }
+
+ response.json({ data: resource });
+ }),
+ );
+
+ app.delete(
+ "/resources/:id",
+ asyncRoute(async (request, response) => {
+ const deleted = await repository.delete(readRouteId(request));
+
+ if (!deleted) {
+ throw new HttpError(404, "Resource not found");
+ }
+
+ response.status(204).send();
+ }),
+ );
+
+ app.use(errorHandler);
+
+ return app;
+}
+
+/**
+ * @description Renders a simple browser-friendly API home page.
+ * @param _request - Incoming Express request.
+ * @param response - Express response used to send HTML.
+ */
+function renderHomePage(_request: Request, response: Response): void {
+ response.type("html").send(`
+
+
+
+
+
+ Problem 5 API
+
+
+
+ Problem 5 API is running
+ Use the resources API to create, list, view, update, and delete resources.
+
+
+
+
+ `);
+}
+
+/**
+ * @description Returns a health-check response for uptime checks.
+ * @param _request - Incoming Express request.
+ * @param response - Express response used to send JSON.
+ */
+function healthCheck(_request: Request, response: Response): void {
+ response.json({ ok: true });
+}
+
+/**
+ * @description Wraps async route handlers and forwards errors to Express.
+ * @param handler - Async route handler to execute.
+ * @returns Express-compatible request handler.
+ */
+function asyncRoute(
+ handler: (request: Request, response: Response) => Promise,
+): RequestHandler {
+ return (request, response, next) => {
+ handler(request, response).catch(next);
+ };
+}
diff --git a/src/problem5/constants.ts b/src/problem5/constants.ts
new file mode 100644
index 0000000000..661ff6ad5e
--- /dev/null
+++ b/src/problem5/constants.ts
@@ -0,0 +1,5 @@
+import path from "node:path";
+
+export const RESOURCE_STATUSES = ["active", "archived"] as const;
+export const DEFAULT_PORT = 3000;
+export const DEFAULT_DATABASE_FILE = path.join(__dirname, "database.json");
diff --git a/src/problem5/errors.ts b/src/problem5/errors.ts
new file mode 100644
index 0000000000..f732ff0897
--- /dev/null
+++ b/src/problem5/errors.ts
@@ -0,0 +1,36 @@
+import { type ErrorRequestHandler, type NextFunction, type Request, type Response } from "express";
+
+/**
+ * @description Represents an HTTP-safe error returned by the API layer.
+ */
+export class HttpError extends Error {
+ /**
+ * @description Creates an error with a response status code.
+ * @param statusCode - HTTP status code sent to the client.
+ * @param message - Public error message sent to the client.
+ */
+ public constructor(
+ public readonly statusCode: number,
+ message: string,
+ ) {
+ super(message);
+ }
+}
+
+/**
+ * @description Converts known application errors into JSON responses.
+ */
+export const errorHandler: ErrorRequestHandler = (
+ error: unknown,
+ _request: Request,
+ response: Response,
+ _next: NextFunction,
+) => {
+ if (error instanceof HttpError) {
+ response.status(error.statusCode).json({ error: error.message });
+ return;
+ }
+
+ console.error(error);
+ response.status(500).json({ error: "Internal server error" });
+};
diff --git a/src/problem5/index.test.ts b/src/problem5/index.test.ts
new file mode 100644
index 0000000000..498e72e92c
--- /dev/null
+++ b/src/problem5/index.test.ts
@@ -0,0 +1,105 @@
+import assert from "node:assert/strict";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { createApp, JsonResourceDatabase } from "./index";
+
+test("Problem 5 CRUD flow persists resources and supports filters", async () => {
+ const tempDirectory = await mkdtemp(path.join(tmpdir(), "problem5-"));
+ const databaseFile = path.join(tempDirectory, "database.json");
+ const app = createApp(new JsonResourceDatabase(databaseFile));
+ const server = app.listen(0);
+
+ try {
+ const address = server.address();
+ assert(address && typeof address === "object");
+
+ const baseUrl = `http://127.0.0.1:${address.port}`;
+
+ const created = await request(baseUrl, "/resources", {
+ method: "POST",
+ body: {
+ title: "Interview preparation notes",
+ description: "Backend CRUD practice task",
+ category: "study",
+ tags: ["typescript", "express"],
+ },
+ });
+
+ assert.equal(created.status, 201);
+ assert.equal(created.body.data.title, "Interview preparation notes");
+
+ const id = created.body.data.id as string;
+ const filtered = await request(
+ baseUrl,
+ "/resources?category=study&tag=typescript&search=backend",
+ );
+
+ assert.equal(filtered.status, 200);
+ assert.equal(filtered.body.count, 1);
+
+ const detail = await request(baseUrl, `/resources/${id}`);
+ assert.equal(detail.status, 200);
+ assert.equal(detail.body.data.id, id);
+
+ const updated = await request(baseUrl, `/resources/${id}`, {
+ method: "PATCH",
+ body: { status: "archived" },
+ });
+
+ assert.equal(updated.status, 200);
+ assert.equal(updated.body.data.status, "archived");
+
+ const deleted = await request(baseUrl, `/resources/${id}`, { method: "DELETE" });
+ assert.equal(deleted.status, 204);
+
+ const missing = await request(baseUrl, `/resources/${id}`);
+ assert.equal(missing.status, 404);
+ } finally {
+ await new Promise((resolve, reject) => {
+ server.close((error) => (error ? reject(error) : resolve()));
+ });
+ await rm(tempDirectory, { recursive: true, force: true });
+ }
+});
+
+test("Problem 5 validates request bodies", async () => {
+ const app = createApp(new JsonResourceDatabase(path.join(tmpdir(), "problem5-validation.json")));
+ const server = app.listen(0);
+
+ try {
+ const address = server.address();
+ assert(address && typeof address === "object");
+
+ const response = await request(`http://127.0.0.1:${address.port}`, "/resources", {
+ method: "POST",
+ body: { category: "study" },
+ });
+
+ assert.equal(response.status, 400);
+ assert.equal(response.body.error, "title is required and must be a non-empty string");
+ } finally {
+ await new Promise((resolve, reject) => {
+ server.close((error) => (error ? reject(error) : resolve()));
+ });
+ }
+});
+
+async function request(
+ baseUrl: string,
+ pathname: string,
+ options: { method?: string; body?: unknown } = {},
+): Promise<{ status: number; body: any }> {
+ const response = await fetch(`${baseUrl}${pathname}`, {
+ method: options.method ?? "GET",
+ headers: options.body === undefined ? undefined : { "Content-Type": "application/json" },
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ });
+ const text = await response.text();
+
+ return {
+ status: response.status,
+ body: text ? JSON.parse(text) : null,
+ };
+}
diff --git a/src/problem5/index.ts b/src/problem5/index.ts
new file mode 100644
index 0000000000..824d55c681
--- /dev/null
+++ b/src/problem5/index.ts
@@ -0,0 +1,27 @@
+/**
+ * @file Problem 5 - A Crude Server
+ * @description Starts and exports a TypeScript ExpressJS CRUD API with a
+ * file-backed database for resource persistence.
+ * @author Paing Sett Kyaw
+ * @created 2026-05-15
+ * @updated 2026-05-16
+ */
+
+import { createApp } from "./app";
+import { DEFAULT_DATABASE_FILE, DEFAULT_PORT } from "./constants";
+import { JsonResourceDatabase } from "./json-resource-database";
+
+if (require.main === module) {
+ const port = Number(process.env.PORT ?? DEFAULT_PORT);
+ const databaseFile = process.env.DATABASE_FILE ?? DEFAULT_DATABASE_FILE;
+ const app = createApp(new JsonResourceDatabase(databaseFile));
+
+ app.listen(port, () => {
+ console.log(`Problem 5 server is running on http://localhost:${port}`);
+ console.log(`Database file: ${databaseFile}`);
+ });
+}
+
+export { createApp } from "./app";
+export { JsonResourceDatabase } from "./json-resource-database";
+export type { Resource, ResourceRepository } from "./types";
diff --git a/src/problem5/json-resource-database.ts b/src/problem5/json-resource-database.ts
new file mode 100644
index 0000000000..8cd1738517
--- /dev/null
+++ b/src/problem5/json-resource-database.ts
@@ -0,0 +1,172 @@
+import { randomUUID } from "node:crypto";
+import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
+import path from "node:path";
+import { DEFAULT_DATABASE_FILE } from "./constants";
+import { HttpError } from "./errors";
+import {
+ type CreateResourceInput,
+ type DatabaseShape,
+ type Resource,
+ type ResourceFilters,
+ type ResourceRepository,
+ type UpdateResourceInput,
+} from "./types";
+import { parseDatabase, resourceContains } from "./validation";
+
+/**
+ * @description File-backed repository for resource records. It stores all
+ * resources in a JSON file and rewrites the file after each mutation.
+ */
+export class JsonResourceDatabase implements ResourceRepository {
+ /**
+ * @description Creates a JSON repository instance.
+ * @param databaseFile - Path to the JSON file used for persistence.
+ */
+ public constructor(private readonly databaseFile = DEFAULT_DATABASE_FILE) {}
+
+ /**
+ * @description Lists resources that match optional filters.
+ * @param filters - Category, status, tag, and search filters.
+ * @returns Matching resources from the database file.
+ */
+ public async list(filters: ResourceFilters): Promise {
+ const { resources } = await this.read();
+ const search = filters.search?.toLowerCase();
+
+ return resources.filter((resource) => {
+ const matchesCategory =
+ filters.category === undefined || resource.category === filters.category;
+ const matchesStatus =
+ filters.status === undefined || resource.status === filters.status;
+ const matchesTag = filters.tag === undefined || resource.tags.includes(filters.tag);
+ const matchesSearch = search === undefined || resourceContains(resource, search);
+
+ return matchesCategory && matchesStatus && matchesTag && matchesSearch;
+ });
+ }
+
+ /**
+ * @description Finds one resource by id.
+ * @param id - Resource id.
+ * @returns The matching resource, or undefined when not found.
+ */
+ public async findById(id: string): Promise {
+ const { resources } = await this.read();
+
+ return resources.find((resource) => resource.id === id);
+ }
+
+ /**
+ * @description Creates and persists a new resource.
+ * @param input - Validated resource fields.
+ * @returns The created resource with id and timestamps.
+ */
+ public async create(input: CreateResourceInput): Promise {
+ const database = await this.read();
+ const timestamp = new Date().toISOString();
+ const resource: Resource = {
+ id: randomUUID(),
+ ...input,
+ createdAt: timestamp,
+ updatedAt: timestamp,
+ };
+
+ await this.write({ resources: [...database.resources, resource] });
+
+ return resource;
+ }
+
+ /**
+ * @description Updates an existing resource and persists the change.
+ * @param id - Resource id.
+ * @param input - Validated partial resource fields.
+ * @returns Updated resource, or undefined when not found.
+ */
+ public async update(
+ id: string,
+ input: UpdateResourceInput,
+ ): Promise {
+ const database = await this.read();
+ const existing = database.resources.find((resource) => resource.id === id);
+
+ if (!existing) {
+ return undefined;
+ }
+
+ const updated: Resource = {
+ ...existing,
+ ...input,
+ updatedAt: new Date().toISOString(),
+ };
+
+ await this.write({
+ resources: database.resources.map((resource) =>
+ resource.id === id ? updated : resource,
+ ),
+ });
+
+ return updated;
+ }
+
+ /**
+ * @description Deletes a resource by id.
+ * @param id - Resource id.
+ * @returns True when a record was removed.
+ */
+ public async delete(id: string): Promise {
+ const database = await this.read();
+ const resources = database.resources.filter((resource) => resource.id !== id);
+
+ if (resources.length === database.resources.length) {
+ return false;
+ }
+
+ await this.write({ resources });
+
+ return true;
+ }
+
+ /**
+ * @description Reads and validates the JSON database file.
+ * @returns Parsed database content, or an empty database when the file is missing.
+ */
+ private async read(): Promise {
+ try {
+ const rawDatabase = await readFile(this.databaseFile, "utf8");
+ const parsedDatabase = JSON.parse(rawDatabase) as unknown;
+
+ return parseDatabase(parsedDatabase);
+ } catch (error) {
+ if (isNodeError(error) && error.code === "ENOENT") {
+ return { resources: [] };
+ }
+
+ if (error instanceof SyntaxError) {
+ throw new HttpError(500, "Database file contains invalid JSON");
+ }
+
+ throw error;
+ }
+ }
+
+ /**
+ * @description Persists the full database using an atomic rename.
+ * @param database - Complete database state to write.
+ */
+ private async write(database: DatabaseShape): Promise {
+ await mkdir(path.dirname(this.databaseFile), { recursive: true });
+
+ const temporaryFile = `${this.databaseFile}.${process.pid}.tmp`;
+ await writeFile(temporaryFile, `${JSON.stringify(database, null, 2)}\n`, "utf8");
+ await rename(temporaryFile, this.databaseFile);
+ }
+}
+
+/**
+ * @description Checks whether an unknown error has a Node-style error code.
+ * @param error - Unknown error thrown by filesystem APIs.
+ * @returns True when the error exposes a code property.
+ */
+function isNodeError(error: unknown): error is NodeJS.ErrnoException {
+ return error instanceof Error && "code" in error;
+}
diff --git a/src/problem5/types.ts b/src/problem5/types.ts
new file mode 100644
index 0000000000..00018984fc
--- /dev/null
+++ b/src/problem5/types.ts
@@ -0,0 +1,40 @@
+import { RESOURCE_STATUSES } from "./constants";
+
+export type ResourceStatus = (typeof RESOURCE_STATUSES)[number];
+
+export interface Resource {
+ id: string;
+ title: string;
+ description: string;
+ category: string;
+ status: ResourceStatus;
+ tags: string[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface DatabaseShape {
+ resources: Resource[];
+}
+
+export interface ResourceFilters {
+ category?: string;
+ status?: ResourceStatus;
+ search?: string;
+ tag?: string;
+}
+
+export type CreateResourceInput = Pick<
+ Resource,
+ "title" | "description" | "category" | "status" | "tags"
+>;
+
+export type UpdateResourceInput = Partial;
+
+export interface ResourceRepository {
+ list(filters: ResourceFilters): Promise;
+ findById(id: string): Promise;
+ create(input: CreateResourceInput): Promise;
+ update(id: string, input: UpdateResourceInput): Promise;
+ delete(id: string): Promise;
+}
diff --git a/src/problem5/validation.ts b/src/problem5/validation.ts
new file mode 100644
index 0000000000..996276f1c5
--- /dev/null
+++ b/src/problem5/validation.ts
@@ -0,0 +1,266 @@
+import { type Request } from "express";
+import { RESOURCE_STATUSES } from "./constants";
+import { HttpError } from "./errors";
+import {
+ type CreateResourceInput,
+ type DatabaseShape,
+ type Resource,
+ type ResourceFilters,
+ type ResourceStatus,
+ type UpdateResourceInput,
+} from "./types";
+
+/**
+ * @description Parses and validates list-resource query filters.
+ * @param request - Express request containing query parameters.
+ * @returns Normalized filters for the repository.
+ */
+export function parseResourceFilters(request: Request): ResourceFilters {
+ const status = readOptionalQueryString(request, "status");
+
+ if (status !== undefined && !isResourceStatus(status)) {
+ throw new HttpError(400, "status must be active or archived");
+ }
+
+ return {
+ category: readOptionalQueryString(request, "category"),
+ status,
+ search: readOptionalQueryString(request, "search"),
+ tag: readOptionalQueryString(request, "tag"),
+ };
+}
+
+/**
+ * @description Parses and validates a create-resource request body.
+ * @param body - Raw JSON request body.
+ * @returns Validated input for creating a resource.
+ */
+export function parseCreateResourceInput(body: unknown): CreateResourceInput {
+ const payload = readObjectPayload(body);
+
+ return {
+ title: readRequiredString(payload.title, "title"),
+ description: readOptionalString(payload.description, "description") ?? "",
+ category: readRequiredString(payload.category, "category"),
+ status: readOptionalStatus(payload.status) ?? "active",
+ tags: readTags(payload.tags),
+ };
+}
+
+/**
+ * @description Parses and validates an update-resource request body.
+ * @param body - Raw JSON request body.
+ * @returns Validated input containing at least one field to update.
+ */
+export function parseUpdateResourceInput(body: unknown): UpdateResourceInput {
+ const payload = readObjectPayload(body);
+ const input: UpdateResourceInput = {};
+
+ if (Object.hasOwn(payload, "title")) {
+ input.title = readRequiredString(payload.title, "title");
+ }
+
+ if (Object.hasOwn(payload, "description")) {
+ input.description = readOptionalString(payload.description, "description") ?? "";
+ }
+
+ if (Object.hasOwn(payload, "category")) {
+ input.category = readRequiredString(payload.category, "category");
+ }
+
+ if (Object.hasOwn(payload, "status")) {
+ input.status = readOptionalStatus(payload.status) ?? "active";
+ }
+
+ if (Object.hasOwn(payload, "tags")) {
+ input.tags = readTags(payload.tags);
+ }
+
+ if (Object.keys(input).length === 0) {
+ throw new HttpError(400, "At least one field must be provided for update");
+ }
+
+ return input;
+}
+
+/**
+ * @description Reads and validates a route resource id.
+ * @param request - Express request containing route parameters.
+ * @returns Trimmed route id.
+ */
+export function readRouteId(request: Request): string {
+ const id = request.params.id;
+
+ if (typeof id !== "string" || id.trim() === "") {
+ throw new HttpError(400, "Resource id is required");
+ }
+
+ return id.trim();
+}
+
+/**
+ * @description Parses the JSON database object loaded from disk.
+ * @param value - Raw parsed JSON value.
+ * @returns Safe database object used by the repository.
+ */
+export function parseDatabase(value: unknown): DatabaseShape {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ throw new HttpError(500, "Database root must be an object");
+ }
+
+ const resources = (value as Partial).resources;
+
+ if (!Array.isArray(resources)) {
+ return { resources: [] };
+ }
+
+ return { resources: resources.filter(isResource) };
+}
+
+/**
+ * @description Checks whether a resource contains the lower-case search term.
+ * @param resource - Resource to search.
+ * @param search - Lower-case search string.
+ * @returns True when any searchable field contains the term.
+ */
+export function resourceContains(resource: Resource, search: string): boolean {
+ return [resource.title, resource.description, resource.category, ...resource.tags]
+ .join(" ")
+ .toLowerCase()
+ .includes(search);
+}
+
+/**
+ * @description Checks whether a value is an allowed resource status.
+ * @param value - Raw status value.
+ * @returns True when the value is active or archived.
+ */
+export function isResourceStatus(value: unknown): value is ResourceStatus {
+ return RESOURCE_STATUSES.includes(value as ResourceStatus);
+}
+
+/**
+ * @description Ensures a request body is a JSON object.
+ * @param value - Raw request body value.
+ * @returns Object payload with unknown field values.
+ */
+function readObjectPayload(value: unknown): Record {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ throw new HttpError(400, "Request body must be a JSON object");
+ }
+
+ return value as Record;
+}
+
+/**
+ * @description Reads one optional string query parameter.
+ * @param request - Express request containing query parameters.
+ * @param key - Query parameter name.
+ * @returns Trimmed query value, or undefined when omitted.
+ */
+function readOptionalQueryString(request: Request, key: string): string | undefined {
+ const value = request.query[key];
+
+ if (value === undefined || value === null || value === "") {
+ return undefined;
+ }
+
+ if (typeof value !== "string") {
+ throw new HttpError(400, `${key} must be a single string value`);
+ }
+
+ return value.trim() || undefined;
+}
+
+/**
+ * @description Validates a required non-empty string field.
+ * @param value - Raw field value.
+ * @param field - Field name used in validation errors.
+ * @returns Trimmed string value.
+ */
+function readRequiredString(value: unknown, field: string): string {
+ const normalized = readOptionalString(value, field);
+
+ if (!normalized) {
+ throw new HttpError(400, `${field} is required and must be a non-empty string`);
+ }
+
+ return normalized;
+}
+
+/**
+ * @description Validates an optional string field.
+ * @param value - Raw field value.
+ * @param field - Field name used in validation errors.
+ * @returns Trimmed string value, or undefined when absent.
+ */
+function readOptionalString(value: unknown, field: string): string | undefined {
+ if (value === undefined || value === null) {
+ return undefined;
+ }
+
+ if (typeof value !== "string") {
+ throw new HttpError(400, `${field} must be a string`);
+ }
+
+ return value.trim();
+}
+
+/**
+ * @description Validates an optional status field.
+ * @param value - Raw status value.
+ * @returns Valid status, or undefined when omitted.
+ */
+function readOptionalStatus(value: unknown): ResourceStatus | undefined {
+ if (value === undefined || value === null || value === "") {
+ return undefined;
+ }
+
+ if (!isResourceStatus(value)) {
+ throw new HttpError(400, "status must be active or archived");
+ }
+
+ return value;
+}
+
+/**
+ * @description Validates an optional tags array.
+ * @param value - Raw tags value.
+ * @returns Trimmed tag strings.
+ */
+function readTags(value: unknown): string[] {
+ if (value === undefined || value === null) {
+ return [];
+ }
+
+ if (!Array.isArray(value)) {
+ throw new HttpError(400, "tags must be an array of strings");
+ }
+
+ return value.map((tag) => readRequiredString(tag, "tag"));
+}
+
+/**
+ * @description Checks whether an unknown value has the persisted resource shape.
+ * @param value - Raw database array item.
+ * @returns True when the value is a valid resource.
+ */
+function isResource(value: unknown): value is Resource {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ return false;
+ }
+
+ const candidate = value as Partial;
+
+ return (
+ typeof candidate.id === "string" &&
+ typeof candidate.title === "string" &&
+ typeof candidate.description === "string" &&
+ typeof candidate.category === "string" &&
+ isResourceStatus(candidate.status) &&
+ Array.isArray(candidate.tags) &&
+ candidate.tags.every((tag) => typeof tag === "string") &&
+ typeof candidate.createdAt === "string" &&
+ typeof candidate.updatedAt === "string"
+ );
+}
diff --git a/src/problem6/.keep b/src/problem6/.keep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/problem6/README.md b/src/problem6/README.md
new file mode 100644
index 0000000000..647d2fa79c
--- /dev/null
+++ b/src/problem6/README.md
@@ -0,0 +1,621 @@
+# Problem 6 - Scoreboard API Architecture
+
+## Purpose
+
+This document specifies the backend module responsible for recording completed
+user actions, increasing user scores, returning the top 10 scoreboard, and
+publishing live scoreboard updates to connected clients.
+
+The module belongs to the application server API. The website calls this module
+after a user completes an action. The module must verify the user, prevent
+unauthorized or duplicated score updates, persist the score change safely, and
+notify live scoreboard clients.
+
+## Scope
+
+This specification covers:
+
+- API endpoint contract for completing a score-increasing action.
+- Authentication and authorization requirements.
+- Validation and anti-abuse rules.
+- Database transaction behavior.
+- Top 10 leaderboard query behavior.
+- Live update publishing through WebSocket or Server-Sent Events.
+- Failure behavior and improvement notes.
+
+This specification does not define the product-specific action itself. The
+module only needs enough action metadata to validate that the action is valid,
+allowed, and not already counted.
+
+## Recommended Technology
+
+The backend implementation should use:
+
+- NestJS for the API module structure, guards, validation pipes, services, and
+ WebSocket gateway.
+- PostgreSQL as the source of truth for users, scores, completed actions, and
+ leaderboard queries.
+- TypeORM, Prisma, or another NestJS-compatible PostgreSQL ORM. Prisma is a good
+ choice for strict schema management, while TypeORM is also acceptable if the
+ existing backend already uses it.
+- `class-validator` and `class-transformer` for request DTO validation.
+- Passport JWT or an existing NestJS auth guard for user authentication.
+- `@nestjs/throttler` or API gateway rate limiting for anti-abuse protection.
+- Redis is optional for caching, distributed rate limits, and WebSocket scaling,
+ but PostgreSQL must remain the source of truth.
+
+## Backend Architecture Design
+
+The backend should be designed as a NestJS `ScoresModule` inside the application
+server. PostgreSQL is the authoritative data store. Redis is optional, but it is
+recommended for distributed rate limiting, leaderboard cache, and WebSocket
+fan-out when the API runs on more than one instance.
+
+```mermaid
+flowchart LR
+ Browser[Website / Scoreboard UI]
+ AuthGuard[JWT Auth Guard]
+ RateGuard[Rate Limit Guard]
+ Controller[ScoresController]
+ ScoresService[ScoresService]
+ LeaderboardService[LeaderboardService]
+ EventsGateway[ScoreEventsGateway]
+ ActionRepo[ScoreAction Repository]
+ CompletedRepo[CompletedAction Repository]
+ ScoreRepo[UserScore Repository]
+ Postgres[(PostgreSQL)]
+ Redis[(Redis optional)]
+ Clients[Connected Scoreboard Clients]
+
+ Browser -->|POST /scores/actions/complete| AuthGuard
+ AuthGuard --> RateGuard
+ RateGuard --> Controller
+ Controller --> ScoresService
+ ScoresService --> ActionRepo
+ ScoresService --> CompletedRepo
+ ScoresService --> ScoreRepo
+ ScoresService --> LeaderboardService
+ ActionRepo --> Postgres
+ CompletedRepo --> Postgres
+ ScoreRepo --> Postgres
+ LeaderboardService --> Postgres
+ LeaderboardService -. cache top 10 .-> Redis
+ ScoresService -->|after commit| EventsGateway
+ EventsGateway -. publish / subscribe .-> Redis
+ EventsGateway -->|leaderboard.updated| Clients
+```
+
+### Backend Layers
+
+| Layer | Responsibility |
+| --- | --- |
+| `ScoresController` | Exposes HTTP endpoints and maps request DTOs to service calls. |
+| `JwtAuthGuard` | Verifies the user identity before any score-changing logic runs. |
+| `ScoreRateLimitGuard` | Blocks excessive requests by user ID, IP address, and action ID. |
+| `ScoresService` | Owns the action completion workflow and PostgreSQL transaction. |
+| `LeaderboardService` | Reads the top 10 leaderboard and optional user rank. |
+| Repositories or ORM delegates | Encapsulate PostgreSQL access for score actions, completed actions, and user scores. |
+| `ScoreEventsGateway` | Publishes live leaderboard updates through WebSocket or SSE. |
+| PostgreSQL | Source of truth for actions, completed action records, scores, and leaderboard queries. |
+| Redis | Optional cache, distributed throttle store, and pub/sub adapter for scaled realtime updates. |
+
+### Backend Request Lifecycle
+
+1. `ScoresController.completeAction()` receives `POST /scores/actions/complete`.
+2. `JwtAuthGuard` validates the JWT and attaches the authenticated user to the
+ request context.
+3. `ScoreRateLimitGuard` checks user/IP/action request frequency.
+4. `ValidationPipe` validates `CompleteScoreActionDto` and removes unknown
+ fields.
+5. `ScoresService.completeAction()` starts a PostgreSQL transaction.
+6. The service loads the server-side `score_actions` record and rejects inactive
+ or unauthorized actions.
+7. The service inserts `completed_score_actions` with unique constraints to
+ prevent duplicate scoring.
+8. The service increments `user_scores.score` by the server-owned
+ `score_delta`.
+9. The service reads the updated user score and top 10 leaderboard.
+10. After transaction commit, `ScoreEventsGateway` publishes
+ `leaderboard.updated` to connected clients.
+11. The API returns the updated user score and leaderboard to the requesting
+ website.
+
+### Important Design Decisions
+
+- The client never sends score delta or final score. It only sends the completed
+ action identity and completion event identity.
+- Duplicate prevention must be enforced by PostgreSQL unique constraints, not
+ only by application code.
+- Score update and completed-action insert must happen in the same transaction.
+- Live events must be emitted only after successful commit.
+- The leaderboard payload must contain only public display fields.
+- If the product later has multiple application server instances, the WebSocket
+ gateway needs a Redis adapter or message broker so every instance receives the
+ same leaderboard update.
+
+## Execution Flow
+
+```mermaid
+sequenceDiagram
+ autonumber
+
+ participant User
+ participant Web as Website / Scoreboard UI
+ participant API as Application Server API
+ participant Auth as Auth Service
+ participant DB as Database
+ participant Live as WebSocket / SSE Channel
+ participant Clients as Connected Scoreboard Clients
+
+ User->>Web: Complete an action
+ Web->>API: POST /scores/actions/complete
+ Note over Web,API: Request includes JWT/session token
and action completion payload
+
+ API->>Auth: Validate user identity and authorization
+ Auth-->>API: User is authenticated
+
+ API->>API: Validate action rules
+ Note over API: Check action exists
Check user is allowed
Check action was not already counted
Apply rate limits / anti-abuse rules
+
+ API->>DB: Start transaction
+ API->>DB: Record completed action
+ API->>DB: Increment user's score
+ API->>DB: Query updated top 10 scores
+ DB-->>API: Updated leaderboard
+ API->>DB: Commit transaction
+
+ API-->>Web: Return updated user score and leaderboard
+
+ API->>Live: Publish leaderboard update
+ Live->>Clients: Push latest top 10 scores
+
+ Clients->>Clients: Re-render live scoreboard
+```
+
+## API Contract
+
+Recommended NestJS route ownership:
+
+```text
+ScoresModule
+ ScoresController
+ ScoresService
+ LeaderboardService
+ ScoreEventsGateway
+ ScoreActionRepository
+```
+
+The route should be protected by an auth guard before entering controller
+business logic.
+
+### Complete Action
+
+`POST /scores/actions/complete`
+
+Records a completed user action and increments the authenticated user's score.
+
+### Headers
+
+```http
+Authorization: Bearer
+Content-Type: application/json
+Idempotency-Key:
+```
+
+`Idempotency-Key` is recommended. If the same request is retried because of a
+network issue, the API should return the original result instead of increasing
+the score again.
+
+### Request Body
+
+```json
+{
+ "actionId": "daily-login",
+ "completionId": "client-or-system-generated-unique-id",
+ "completedAt": "2026-05-15T15:30:00.000Z",
+ "metadata": {
+ "source": "web"
+ }
+}
+```
+
+### Request Fields
+
+| Field | Type | Required | Description |
+| --- | --- | --- | --- |
+| `actionId` | string | Yes | Identifier of the action that was completed. |
+| `completionId` | string | Yes | Unique identifier for this completion event. Used to prevent duplicates. |
+| `completedAt` | ISO datetime | Yes | Time the action was completed on the client or source system. |
+| `metadata` | object | No | Optional non-sensitive details for validation, auditing, or analytics. |
+
+The API must not trust score values from the client. The score increment must be
+looked up from server-side action configuration.
+
+### NestJS DTO
+
+```ts
+import { IsDateString, IsObject, IsOptional, IsString, MaxLength } from "class-validator";
+
+export class CompleteScoreActionDto {
+ @IsString()
+ @MaxLength(100)
+ actionId: string;
+
+ @IsString()
+ @MaxLength(150)
+ completionId: string;
+
+ @IsDateString()
+ completedAt: string;
+
+ @IsOptional()
+ @IsObject()
+ metadata?: Record;
+}
+```
+
+### Success Response
+
+`200 OK`
+
+```json
+{
+ "userScore": {
+ "userId": "user_123",
+ "score": 120,
+ "rank": 8
+ },
+ "leaderboard": [
+ {
+ "rank": 1,
+ "userId": "user_001",
+ "displayName": "Alice",
+ "score": 320
+ }
+ ]
+}
+```
+
+The leaderboard array must contain at most 10 records ordered by score
+descending. Ties should use a stable secondary order such as earliest score
+update time, then user ID.
+
+## Validation Rules
+
+The API must validate the request in this order:
+
+1. Authenticate the JWT or session token.
+2. Confirm the authenticated user is allowed to complete the requested action.
+3. Confirm `actionId` exists and is active.
+4. Confirm the action has not already been counted for this user and completion
+ event.
+5. Confirm the action-specific frequency rule is satisfied, for example once
+ per day or once per campaign.
+6. Apply rate limits based on user ID, IP address, and action ID.
+7. Reject suspicious metadata or invalid timestamps.
+
+The server should return deterministic responses for duplicate requests. A
+previously accepted duplicate should not increment score again.
+
+In NestJS, the authentication check should be handled by a guard, while request
+shape validation should be handled by a global `ValidationPipe` with
+`whitelist: true` and `forbidNonWhitelisted: true`.
+
+## Persistence Model
+
+Recommended PostgreSQL tables:
+
+### `users`
+
+| Column | Description |
+| --- | --- |
+| `id` | Primary user identifier. |
+| `display_name` | Public display name shown on the scoreboard. |
+| `created_at` | User creation time. |
+
+### `score_actions`
+
+| Column | Description |
+| --- | --- |
+| `id` | Action identifier. |
+| `score_delta` | Number of points awarded when completed. |
+| `is_active` | Whether the action can currently award score. |
+| `frequency_rule` | Rule used to prevent repeated scoring. |
+
+### `user_scores`
+
+| Column | Description |
+| --- | --- |
+| `user_id` | User identifier. Unique. |
+| `score` | Current total score. |
+| `updated_at` | Last score update time. |
+
+### `completed_score_actions`
+
+| Column | Description |
+| --- | --- |
+| `id` | Internal record ID. |
+| `user_id` | User who completed the action. |
+| `action_id` | Completed action. |
+| `completion_id` | Unique completion event ID. |
+| `idempotency_key` | Request idempotency key. |
+| `score_delta` | Score awarded for this completion. |
+| `created_at` | Server-side record creation time. |
+
+Required uniqueness constraints:
+
+- Unique `(user_id, action_id, completion_id)`.
+- Unique `(user_id, idempotency_key)` when an idempotency key is provided.
+- Additional unique indexes for frequency rules, for example
+ `(user_id, action_id, action_date)` for once-per-day actions.
+
+Example PostgreSQL DDL:
+
+```sql
+CREATE TABLE score_actions (
+ id VARCHAR(100) PRIMARY KEY,
+ score_delta INTEGER NOT NULL CHECK (score_delta > 0),
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ frequency_rule VARCHAR(50) NOT NULL DEFAULT 'once',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE user_scores (
+ user_id UUID PRIMARY KEY REFERENCES users(id),
+ score INTEGER NOT NULL DEFAULT 0 CHECK (score >= 0),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE completed_score_actions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ user_id UUID NOT NULL REFERENCES users(id),
+ action_id VARCHAR(100) NOT NULL REFERENCES score_actions(id),
+ completion_id VARCHAR(150) NOT NULL,
+ idempotency_key VARCHAR(150),
+ score_delta INTEGER NOT NULL CHECK (score_delta > 0),
+ metadata JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE (user_id, action_id, completion_id),
+ UNIQUE (user_id, idempotency_key)
+);
+
+CREATE INDEX idx_user_scores_leaderboard
+ ON user_scores (score DESC, updated_at ASC, user_id ASC);
+```
+
+If `idempotency_key` is nullable, PostgreSQL allows multiple null values in a
+unique constraint. That is acceptable because the duplicate protection still
+comes from `(user_id, action_id, completion_id)`.
+
+## Transaction Requirements
+
+The completed action record, user score increment, and leaderboard read must be
+handled in a single database transaction.
+
+Expected transaction steps:
+
+1. Lock or upsert the user's score row.
+2. Insert the completed action record.
+3. If the insert violates a uniqueness rule, return the previous accepted
+ result or a duplicate response without incrementing score.
+4. Increment the user's score by the server-side `score_delta`.
+5. Query the updated top 10 leaderboard.
+6. Commit the transaction.
+
+The live update must be published only after the transaction commits. This
+prevents clients from seeing a leaderboard update that was later rolled back.
+
+Recommended NestJS service behavior:
+
+```ts
+async completeAction(userId: string, dto: CompleteScoreActionDto, idempotencyKey?: string) {
+ const result = await this.database.transaction(async (tx) => {
+ const action = await tx.scoreAction.findActive(dto.actionId);
+ this.assertActionCanBeCompleted(userId, action, dto);
+
+ await tx.completedScoreAction.insert({
+ userId,
+ actionId: dto.actionId,
+ completionId: dto.completionId,
+ idempotencyKey,
+ scoreDelta: action.scoreDelta,
+ metadata: dto.metadata,
+ });
+
+ await tx.userScore.increment(userId, action.scoreDelta);
+
+ return {
+ userScore: await tx.userScore.findUserScoreWithRank(userId),
+ leaderboard: await tx.userScore.findTop10(),
+ };
+ });
+
+ await this.scoreEventsGateway.publishLeaderboardUpdated(result.leaderboard);
+
+ return result;
+}
+```
+
+The real implementation must catch unique-constraint errors and convert them
+into either the previous idempotent response or a `409 Conflict` response.
+
+## Leaderboard Query
+
+The top 10 query should be optimized with an index on:
+
+```sql
+score DESC, updated_at ASC, user_id ASC
+```
+
+The API may calculate the requesting user's rank separately if the user is not
+in the top 10. This is useful for returning the user's updated score while still
+keeping the public scoreboard limited to 10 rows.
+
+Example top 10 query:
+
+```sql
+SELECT
+ ROW_NUMBER() OVER (ORDER BY us.score DESC, us.updated_at ASC, us.user_id ASC) AS rank,
+ us.user_id,
+ u.display_name,
+ us.score
+FROM user_scores us
+JOIN users u ON u.id = us.user_id
+ORDER BY us.score DESC, us.updated_at ASC, us.user_id ASC
+LIMIT 10;
+```
+
+## Live Updates
+
+The live channel may be implemented with WebSocket or Server-Sent Events.
+
+Recommended event name:
+
+`leaderboard.updated`
+
+Recommended event payload:
+
+```json
+{
+ "leaderboard": [
+ {
+ "rank": 1,
+ "userId": "user_001",
+ "displayName": "Alice",
+ "score": 320
+ }
+ ],
+ "updatedAt": "2026-05-15T15:30:01.000Z"
+}
+```
+
+Connected scoreboard clients should replace their current top 10 list with the
+latest payload. The API or live service should avoid publishing sensitive user
+data.
+
+Recommended NestJS implementation:
+
+- Use a `ScoreEventsGateway` if the application already uses WebSocket.
+- Use SSE if the scoreboard only needs one-way server-to-client updates.
+- Emit updates after the database transaction commits.
+- If multiple API instances are deployed, use Redis Pub/Sub or a message broker
+ so every instance can broadcast the same leaderboard update to its connected
+ clients.
+
+## Security Requirements
+
+- Require authentication for score-changing endpoints.
+- Never accept score deltas or final scores from the client.
+- Use server-side action configuration to determine score changes.
+- Store completed action records for auditability.
+- Apply rate limits and duplicate detection before awarding score.
+- Log rejected requests with enough context for abuse investigation.
+- Do not expose private user information in leaderboard responses.
+- Validate all request fields with a strict schema.
+- Use HTTPS in production.
+
+## Error Responses
+
+| Status | Reason |
+| --- | --- |
+| `400 Bad Request` | Missing or invalid request fields. |
+| `401 Unauthorized` | Missing or invalid authentication token. |
+| `403 Forbidden` | User is authenticated but not allowed to complete the action. |
+| `404 Not Found` | Action does not exist or is not visible to this user. |
+| `409 Conflict` | Action completion was already counted and cannot be repeated. |
+| `429 Too Many Requests` | Rate limit or anti-abuse rule was triggered. |
+| `500 Internal Server Error` | Unexpected server failure. |
+
+For duplicate requests caused by retry behavior, prefer returning the original
+successful response when an idempotency key matches.
+
+Recommended NestJS exception mapping:
+
+| Exception | Status |
+| --- | --- |
+| `BadRequestException` | `400 Bad Request` |
+| `UnauthorizedException` | `401 Unauthorized` |
+| `ForbiddenException` | `403 Forbidden` |
+| `NotFoundException` | `404 Not Found` |
+| `ConflictException` | `409 Conflict` |
+| `TooManyRequestsException` | `429 Too Many Requests` |
+
+## Observability
+
+The module should emit structured logs and metrics for:
+
+- Accepted score updates.
+- Duplicate completion attempts.
+- Authorization failures.
+- Rate-limited requests.
+- Transaction failures.
+- Live update publish failures.
+- Leaderboard query latency.
+
+Important metrics:
+
+- `score_action_complete_total`
+- `score_action_duplicate_total`
+- `score_action_rejected_total`
+- `leaderboard_publish_total`
+- `leaderboard_publish_failed_total`
+- `leaderboard_query_duration_ms`
+
+## Additional Comments for Improvement
+
+These improvements are not required for the first implementation, but they
+should be considered before production release:
+
+1. Use the outbox pattern for live update publishing. Store a
+ `leaderboard.updated` event inside the same PostgreSQL transaction, then let a
+ background worker publish it to WebSocket or SSE clients. This prevents lost
+ live updates when the API commits successfully but the publish step fails.
+2. Add Redis for distributed rate limiting. In-memory NestJS throttling is not
+ enough when the API runs on multiple instances.
+3. Cache the top 10 leaderboard in Redis for high traffic. PostgreSQL should
+ remain the source of truth, and cache invalidation should happen after score
+ updates.
+4. Add admin audit tooling for suspicious score changes, duplicate attempts, and
+ rate-limit violations.
+5. Add a score rebuild job that recalculates `user_scores` from
+ `completed_score_actions`. This helps recover from scoring bugs or manual
+ data fixes.
+6. Sign action-completion events if actions are completed by another backend
+ service. The API should verify the signature before awarding score.
+7. Add integration tests that prove duplicate requests, unauthorized requests,
+ and concurrent requests cannot increase the score incorrectly.
+8. Add a rank lookup endpoint later, for example `GET /scores/me`, so users
+ outside the top 10 can still see their own score and rank.
+9. Use database migrations for all tables and indexes. The backend team should
+ not create production tables manually.
+10. Avoid putting private user data in live events. The scoreboard should expose
+ only public display fields required by the UI.
+
+## Suggested NestJS File Structure
+
+```text
+src/scores/
+ dto/
+ complete-score-action.dto.ts
+ entities/
+ completed-score-action.entity.ts
+ score-action.entity.ts
+ user-score.entity.ts
+ scores.controller.ts
+ scores.module.ts
+ scores.service.ts
+ leaderboard.service.ts
+ score-events.gateway.ts
+ score-rate-limit.guard.ts
+```
+
+The exact ORM entity filenames can change depending on whether the backend team
+chooses Prisma or TypeORM, but the module boundaries should stay the same:
+
+- `ScoresController` handles HTTP request and response mapping.
+- `ScoresService` owns the transaction and score update workflow.
+- `LeaderboardService` owns top 10 and rank queries.
+- `ScoreEventsGateway` owns WebSocket or SSE publishing.
+- Guards and DTOs own authentication, authorization, throttling, and request
+ validation.
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..b7ea06cf9a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "CommonJS",
+ "moduleResolution": "Node",
+ "rootDir": "src",
+ "outDir": "dist",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["node_modules", "dist"]
+}