diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..8f77410605
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+node_modules/
+dist/
+build/
+coverage/
+*.tsbuildinfo
+
+.env
+.env.local
+.env.*.local
+
+.DS_Store
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
diff --git a/README copy.md b/README copy.md
new file mode 100644
index 0000000000..2bf943dc27
--- /dev/null
+++ b/README copy.md
@@ -0,0 +1,24 @@
+# My Challenge
+
+This workspace contains my own solutions for:
+
+- `problem4`: three TypeScript implementations of sum-to-n
+- `problem5`: a TypeScript Express + PostgreSQL CRUD API with a concurrency-safe increment endpoint using TypeORM pessimistic locking
+- `problem6`: an implementation-ready architecture package for a real-time scoreboard using PostgreSQL, transactional outbox, CDC/Kafka, Redis projections, and live updates
+
+## Scripts
+
+From this directory:
+
+```bash
+npm install
+npm run test:problem4
+npm run dev:problem5
+npm run test:problem5
+```
+
+## Notes
+
+- Problem 5 expects PostgreSQL for both development and integration tests.
+- See `src/problem5/README.md` for database configuration and the locking test design.
+- Problem 6 is a documentation and system-design deliverable under `src/problem6/`.
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000..f0c82d452c
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2936 @@
+{
+ "name": "my-challenge",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "my-challenge",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.21.2",
+ "pg": "^8.12.0",
+ "reflect-metadata": "^0.2.2",
+ "typeorm": "^0.3.24"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.17",
+ "@types/express": "^4.17.21",
+ "@types/node": "^20.14.11",
+ "ts-node": "^10.9.2",
+ "ts-node-dev": "^2.0.0",
+ "typescript": "^5.6.3"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://npm.vexere.net/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://npm.vexere.net/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://npm.vexere.net/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://npm.vexere.net/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://npm.vexere.net/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://npm.vexere.net/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://npm.vexere.net/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://npm.vexere.net/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://npm.vexere.net/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://npm.vexere.net/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://npm.vexere.net/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://npm.vexere.net/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@sqltools/formatter": {
+ "version": "1.2.5",
+ "resolved": "https://npm.vexere.net/@sqltools/formatter/-/formatter-1.2.5.tgz",
+ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://npm.vexere.net/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://npm.vexere.net/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://npm.vexere.net/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://npm.vexere.net/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://npm.vexere.net/@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://npm.vexere.net/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://npm.vexere.net/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://npm.vexere.net/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://npm.vexere.net/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "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://npm.vexere.net/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://npm.vexere.net/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.39",
+ "resolved": "https://npm.vexere.net/@types/node/-/node-20.19.39.tgz",
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.0",
+ "resolved": "https://npm.vexere.net/@types/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://npm.vexere.net/@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://npm.vexere.net/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://npm.vexere.net/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://npm.vexere.net/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://npm.vexere.net/@types/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/strip-json-comments": {
+ "version": "0.0.30",
+ "resolved": "https://npm.vexere.net/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
+ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://npm.vexere.net/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://npm.vexere.net/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "devOptional": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://npm.vexere.net/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://npm.vexere.net/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://npm.vexere.net/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ansis": {
+ "version": "4.2.0",
+ "resolved": "https://npm.vexere.net/ansis/-/ansis-4.2.0.tgz",
+ "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://npm.vexere.net/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/app-root-path": {
+ "version": "3.1.0",
+ "resolved": "https://npm.vexere.net/app-root-path/-/app-root-path-3.1.0.tgz",
+ "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://npm.vexere.net/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://npm.vexere.net/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
+ "license": "MIT"
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://npm.vexere.net/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/async-generator-function": {
+ "version": "1.0.0",
+ "resolved": "https://npm.vexere.net/async-generator-function/-/async-generator-function-1.0.0.tgz",
+ "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://npm.vexere.net/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://npm.vexere.net/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://npm.vexere.net/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://npm.vexere.net/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://npm.vexere.net/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.14",
+ "resolved": "https://npm.vexere.net/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://npm.vexere.net/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://npm.vexere.net/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://npm.vexere.net/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://npm.vexere.net/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://npm.vexere.net/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/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/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://npm.vexere.net/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://npm.vexere.net/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://npm.vexere.net/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://npm.vexere.net/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://npm.vexere.net/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://npm.vexere.net/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://npm.vexere.net/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://npm.vexere.net/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://npm.vexere.net/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://npm.vexere.net/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://npm.vexere.net/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://npm.vexere.net/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "1.7.2",
+ "resolved": "https://npm.vexere.net/dedent/-/dedent-1.7.2.tgz",
+ "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "babel-plugin-macros": "^3.1.0"
+ },
+ "peerDependenciesMeta": {
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://npm.vexere.net/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://npm.vexere.net/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.4",
+ "resolved": "https://npm.vexere.net/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+ "devOptional": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://npm.vexere.net/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/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/dynamic-dedupe": {
+ "version": "0.3.0",
+ "resolved": "https://npm.vexere.net/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
+ "integrity": "sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE=",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://npm.vexere.net/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://npm.vexere.net/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://npm.vexere.net/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/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://npm.vexere.net/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://npm.vexere.net/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/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://npm.vexere.net/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://npm.vexere.net/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://npm.vexere.net/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://npm.vexere.net/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://npm.vexere.net/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://npm.vexere.net/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://npm.vexere.net/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://npm.vexere.net/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://npm.vexere.net/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://npm.vexere.net/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://npm.vexere.net/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://npm.vexere.net/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://npm.vexere.net/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.1",
+ "resolved": "https://npm.vexere.net/get-intrinsic/-/get-intrinsic-1.3.1.tgz",
+ "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "async-generator-function": "^1.0.0",
+ "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",
+ "generator-function": "^2.0.0",
+ "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://npm.vexere.net/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/glob": {
+ "version": "7.2.3",
+ "resolved": "https://npm.vexere.net/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://npm.vexere.net/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://npm.vexere.net/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-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://npm.vexere.net/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://npm.vexere.net/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://npm.vexere.net/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://npm.vexere.net/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://npm.vexere.net/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.4.24",
+ "resolved": "https://npm.vexere.net/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://npm.vexere.net/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://npm.vexere.net/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://npm.vexere.net/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://npm.vexere.net/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-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://npm.vexere.net/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://npm.vexere.net/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://npm.vexere.net/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://npm.vexere.net/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://npm.vexere.net/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://npm.vexere.net/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://npm.vexere.net/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://npm.vexere.net/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://npm.vexere.net/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://npm.vexere.net/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://npm.vexere.net/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://npm.vexere.net/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://npm.vexere.net/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://npm.vexere.net/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://npm.vexere.net/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://npm.vexere.net/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://npm.vexere.net/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://npm.vexere.net/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://npm.vexere.net/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://npm.vexere.net/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://npm.vexere.net/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://npm.vexere.net/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://npm.vexere.net/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://npm.vexere.net/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://npm.vexere.net/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://npm.vexere.net/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/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://npm.vexere.net/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://npm.vexere.net/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://npm.vexere.net/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://npm.vexere.net/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://npm.vexere.net/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://npm.vexere.net/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "license": "MIT"
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://npm.vexere.net/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://npm.vexere.net/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://npm.vexere.net/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://npm.vexere.net/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://npm.vexere.net/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://npm.vexere.net/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://npm.vexere.net/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://npm.vexere.net/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://npm.vexere.net/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://npm.vexere.net/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://npm.vexere.net/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://npm.vexere.net/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.14.2",
+ "resolved": "https://npm.vexere.net/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "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://npm.vexere.net/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": "2.5.3",
+ "resolved": "https://npm.vexere.net/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://npm.vexere.net/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://npm.vexere.net/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://npm.vexere.net/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://npm.vexere.net/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://npm.vexere.net/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://npm.vexere.net/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://npm.vexere.net/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://npm.vexere.net/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://npm.vexere.net/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://npm.vexere.net/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://npm.vexere.net/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://npm.vexere.net/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.12",
+ "resolved": "https://npm.vexere.net/sha.js/-/sha.js-2.4.12.tgz",
+ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.0"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://npm.vexere.net/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://npm.vexere.net/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://npm.vexere.net/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://npm.vexere.net/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://npm.vexere.net/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/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://npm.vexere.net/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://npm.vexere.net/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://npm.vexere.net/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://npm.vexere.net/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sql-highlight": {
+ "version": "6.1.0",
+ "resolved": "https://npm.vexere.net/sql-highlight/-/sql-highlight-6.1.0.tgz",
+ "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==",
+ "funding": [
+ "https://github.com/scriptcoded/sql-highlight?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/scriptcoded"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://npm.vexere.net/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://npm.vexere.net/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://npm.vexere.net/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://npm.vexere.net/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://npm.vexere.net/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://npm.vexere.net/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://npm.vexere.net/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://npm.vexere.net/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/to-buffer": {
+ "version": "1.2.2",
+ "resolved": "https://npm.vexere.net/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://npm.vexere.net/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://npm.vexere.net/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://npm.vexere.net/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-node-dev": {
+ "version": "2.0.0",
+ "resolved": "https://npm.vexere.net/ts-node-dev/-/ts-node-dev-2.0.0.tgz",
+ "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.1",
+ "dynamic-dedupe": "^0.3.0",
+ "minimist": "^1.2.6",
+ "mkdirp": "^1.0.4",
+ "resolve": "^1.0.0",
+ "rimraf": "^2.6.1",
+ "source-map-support": "^0.5.12",
+ "tree-kill": "^1.2.2",
+ "ts-node": "^10.4.0",
+ "tsconfig": "^7.0.0"
+ },
+ "bin": {
+ "ts-node-dev": "lib/bin.js",
+ "tsnd": "lib/bin.js"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "*",
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tsconfig": {
+ "version": "7.0.0",
+ "resolved": "https://npm.vexere.net/tsconfig/-/tsconfig-7.0.0.tgz",
+ "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/strip-bom": "^3.0.0",
+ "@types/strip-json-comments": "0.0.30",
+ "strip-bom": "^3.0.0",
+ "strip-json-comments": "^2.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://npm.vexere.net/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://npm.vexere.net/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://npm.vexere.net/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typeorm": {
+ "version": "0.3.28",
+ "resolved": "https://npm.vexere.net/typeorm/-/typeorm-0.3.28.tgz",
+ "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==",
+ "license": "MIT",
+ "dependencies": {
+ "@sqltools/formatter": "^1.2.5",
+ "ansis": "^4.2.0",
+ "app-root-path": "^3.1.0",
+ "buffer": "^6.0.3",
+ "dayjs": "^1.11.19",
+ "debug": "^4.4.3",
+ "dedent": "^1.7.0",
+ "dotenv": "^16.6.1",
+ "glob": "^10.5.0",
+ "reflect-metadata": "^0.2.2",
+ "sha.js": "^2.4.12",
+ "sql-highlight": "^6.1.0",
+ "tslib": "^2.8.1",
+ "uuid": "^11.1.0",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "typeorm": "cli.js",
+ "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js",
+ "typeorm-ts-node-esm": "cli-ts-node-esm.js"
+ },
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/typeorm"
+ },
+ "peerDependencies": {
+ "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "@sap/hana-client": "^2.14.22",
+ "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
+ "ioredis": "^5.0.4",
+ "mongodb": "^5.8.0 || ^6.0.0",
+ "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0",
+ "mysql2": "^2.2.5 || ^3.0.1",
+ "oracledb": "^6.3.0",
+ "pg": "^8.5.1",
+ "pg-native": "^3.0.0",
+ "pg-query-stream": "^4.0.0",
+ "redis": "^3.1.1 || ^4.0.0 || ^5.0.14",
+ "sql.js": "^1.4.0",
+ "sqlite3": "^5.0.3",
+ "ts-node": "^10.7.0",
+ "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@google-cloud/spanner": {
+ "optional": true
+ },
+ "@sap/hana-client": {
+ "optional": true
+ },
+ "better-sqlite3": {
+ "optional": true
+ },
+ "ioredis": {
+ "optional": true
+ },
+ "mongodb": {
+ "optional": true
+ },
+ "mssql": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "oracledb": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "pg-native": {
+ "optional": true
+ },
+ "pg-query-stream": {
+ "optional": true
+ },
+ "redis": {
+ "optional": true
+ },
+ "sql.js": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ },
+ "typeorm-aurora-data-api-driver": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/typeorm/node_modules/brace-expansion": {
+ "version": "2.1.0",
+ "resolved": "https://npm.vexere.net/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/typeorm/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://npm.vexere.net/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/typeorm/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://npm.vexere.net/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/typeorm/node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://npm.vexere.net/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/typeorm/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://npm.vexere.net/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://npm.vexere.net/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://npm.vexere.net/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://npm.vexere.net/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://npm.vexere.net/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://npm.vexere.net/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://npm.vexere.net/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://npm.vexere.net/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://npm.vexere.net/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://npm.vexere.net/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://npm.vexere.net/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://npm.vexere.net/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://npm.vexere.net/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://npm.vexere.net/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://npm.vexere.net/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://npm.vexere.net/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://npm.vexere.net/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://npm.vexere.net/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..695335a7b0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "my-challenge",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "test:problem4": "node -r ts-node/register src/problem4/run-tests.ts",
+ "dev:problem5": "ts-node-dev --respawn --transpile-only src/problem5/src/server.ts",
+ "test:problem5": "node --test -r ts-node/register src/problem5/tests/integration/*.test.ts"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.21.2",
+ "pg": "^8.12.0",
+ "reflect-metadata": "^0.2.2",
+ "typeorm": "^0.3.24"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.17",
+ "@types/express": "^4.17.21",
+ "@types/node": "^20.14.11",
+ "ts-node": "^10.9.2",
+ "ts-node-dev": "^2.0.0",
+ "typescript": "^5.6.3"
+ }
+}
diff --git a/src/problem4/.keep b/src/problem2/.keep
similarity index 100%
rename from src/problem4/.keep
rename to src/problem2/.keep
diff --git a/src/problem2/index.html b/src/problem2/index.html
deleted file mode 100644
index 4058a68bff..0000000000
--- a/src/problem2/index.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
- Fancy Form
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/problem2/script.js b/src/problem2/script.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/problem2/style.css b/src/problem2/style.css
deleted file mode 100644
index 915af91c72..0000000000
--- a/src/problem2/style.css
+++ /dev/null
@@ -1,8 +0,0 @@
-body {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- min-width: 360px;
- font-family: Arial, Helvetica, sans-serif;
-}
diff --git a/src/problem4/README.md b/src/problem4/README.md
new file mode 100644
index 0000000000..6ec1667c20
--- /dev/null
+++ b/src/problem4/README.md
@@ -0,0 +1,42 @@
+# Problem 4
+
+## Interpretation
+
+The prompt asks for the summation from `1` to `n`.
+
+For this implementation:
+
+- if `n >= 1`, return `1 + 2 + ... + n`
+- if `n < 1`, return `0` because the range `1..n` contains no positive integers
+- if `n` is not an integer, truncate toward zero first
+
+This keeps the contract consistent with the natural reading of "sum to n" from `1`.
+
+## Implementations
+
+### `sum_to_n_a`
+
+- Strategy: iterative loop
+- Time: `O(n)`
+- Space: `O(1)`
+
+### `sum_to_n_b`
+
+- Strategy: recursion
+- Time: `O(n)`
+- Space: `O(n)`
+
+### `sum_to_n_c`
+
+- Strategy: arithmetic formula
+- Time: `O(1)`
+- Space: `O(1)`
+
+## Run
+
+From the repository root:
+
+```bash
+npm install
+npm run test:problem4
+```
diff --git a/src/problem4/run-tests.ts b/src/problem4/run-tests.ts
new file mode 100644
index 0000000000..053e1737c3
--- /dev/null
+++ b/src/problem4/run-tests.ts
@@ -0,0 +1,34 @@
+import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "./sum_to_n";
+
+type TestCase = {
+ readonly n: number;
+ readonly expected: number;
+};
+
+function assertEqual(actual: number, expected: number, label: string): void {
+ if (actual !== expected) {
+ console.error(`FAIL ${label}: expected ${expected}, received ${actual}`);
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(`OK ${label}: ${actual}`);
+}
+
+const cases: TestCase[] = [
+ { n: 5, expected: 15 },
+ { n: 1, expected: 1 },
+ { n: 0, expected: 0 },
+ { n: -5, expected: 0 },
+ { n: 10.8, expected: 55 }
+];
+
+for (const testCase of cases) {
+ assertEqual(sum_to_n_a(testCase.n), testCase.expected, `sum_to_n_a(${testCase.n})`);
+ assertEqual(sum_to_n_b(testCase.n), testCase.expected, `sum_to_n_b(${testCase.n})`);
+ assertEqual(sum_to_n_c(testCase.n), testCase.expected, `sum_to_n_c(${testCase.n})`);
+}
+
+if (process.exitCode === undefined || process.exitCode === 0) {
+ console.log("All Problem 4 tests passed.");
+}
diff --git a/src/problem4/sum_to_n.ts b/src/problem4/sum_to_n.ts
new file mode 100644
index 0000000000..c1e8841452
--- /dev/null
+++ b/src/problem4/sum_to_n.ts
@@ -0,0 +1,50 @@
+/**
+ * Problem 4: provide three unique implementations for summing integers up to n.
+ *
+ * For this solution:
+ * - If n >= 1, sum 1..n.
+ * - If n < 1, return 0 because there are no positive integers in the range 1..n.
+ * - Decimal inputs are truncated toward zero to keep the contract deterministic.
+ */
+
+/** Iterative loop. Time: O(n), Space: O(1) */
+export function sum_to_n_a(n: number): number {
+ const limit = Math.trunc(n);
+
+ if (limit < 1) {
+ return 0;
+ }
+
+ let total = 0;
+ for (let value = 1; value <= limit; value += 1) {
+ total += value;
+ }
+
+ return total;
+}
+
+/** Recursive decomposition. Time: O(n), Space: O(n) due to call stack */
+export function sum_to_n_b(n: number): number {
+ const limit = Math.trunc(n);
+
+ if (limit < 1) {
+ return 0;
+ }
+
+ if (limit === 1) {
+ return 1;
+ }
+
+ return limit + sum_to_n_b(limit - 1);
+}
+
+/** Arithmetic series formula. Time: O(1), Space: O(1) */
+export function sum_to_n_c(n: number): number {
+ const limit = Math.trunc(n);
+
+ if (limit < 1) {
+ return 0;
+ }
+
+ return (limit * (limit + 1)) / 2;
+}
diff --git a/src/problem5/.env.example b/src/problem5/.env.example
new file mode 100644
index 0000000000..811e78f1b0
--- /dev/null
+++ b/src/problem5/.env.example
@@ -0,0 +1,3 @@
+PORT=3000
+DATABASE_URL=postgres://app:app@127.0.0.1:5433/my_challenge
+TEST_DATABASE_URL=postgres://app:app@127.0.0.1:5433/my_challenge_test
diff --git a/src/problem5/.keep b/src/problem5/.keep
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/src/problem5/README.md b/src/problem5/README.md
new file mode 100644
index 0000000000..22641e9e73
--- /dev/null
+++ b/src/problem5/README.md
@@ -0,0 +1,583 @@
+# Problem 5: Resource Management API
+
+A RESTful API for managing resources built with Express, TypeORM, and PostgreSQL.
+
+This solution satisfies the CRUD requirement and intentionally highlights database skills by adding a concurrency-safe increment endpoint implemented with a transaction and PostgreSQL row locking.
+
+## Overview
+
+This backend provides:
+
+- resource creation
+- resource listing with basic filters
+- resource detail lookup
+- resource updates
+- resource deletion
+- a concurrency-safe quantity increment operation
+
+The implementation uses:
+
+- **Framework**: Express
+- **Language**: TypeScript
+- **ORM**: TypeORM
+- **Database**: PostgreSQL
+- **Runtime DB Setup**: Docker Compose
+- **Concurrency Control**: PostgreSQL `SELECT ... FOR UPDATE` via TypeORM `pessimistic_write`
+
+## Why This Design
+
+The original problem only requires CRUD plus persistence. I intentionally used PostgreSQL instead of SQLite to demonstrate stronger backend engineering around:
+
+- transaction boundaries
+- row-level locking
+- lost-update prevention
+- real integration testing against a real database
+
+The extra endpoint `POST /resources/:id/increment` is the clearest way to show that skill in a coding test.
+
+## Project Structure
+
+```text
+problem5/
+ ├─ .env.example
+ ├─ docker-compose.yml
+ ├─ docker/
+ │ └─ init/
+ │ └─ 01-create-test-db.sql
+ ├─ src/
+ │ ├─ app.ts
+ │ ├─ server.ts
+ │ ├─ config/
+ │ ├─ controllers/
+ │ ├─ db/
+ │ ├─ entities/
+ │ ├─ middleware/
+ │ ├─ repositories/
+ │ ├─ routes/
+ │ ├─ services/
+ │ ├─ types/
+ │ ├─ utils/
+ │ └─ validators/
+ └─ tests/
+ └─ integration/
+```
+
+## Resource Model
+
+Each resource has:
+
+- `id`: auto-generated primary key
+- `code`: required business identifier, unique per resource
+- `name`: required string
+- `description`: optional string, nullable
+- `quantity`: integer field used for CRUD and concurrency-safe increments
+- `createdAt`: timestamp
+- `updatedAt`: timestamp
+
+Example persisted row:
+
+```json
+{
+ "id": 1,
+ "code": "SKU-001",
+ "name": "Warehouse Item",
+ "description": "Primary stock item",
+ "quantity": 10,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:17:11.245Z"
+}
+```
+
+## Prerequisites
+
+- Node.js 20+
+- npm
+- Docker Desktop or a working Docker daemon
+
+## Setup
+
+### 1. Install dependencies
+
+From the repository root:
+
+```bash
+cd my-challenge
+npm install
+```
+
+### 2. Start PostgreSQL in Docker
+
+From the Problem 5 directory:
+
+```bash
+cd my-challenge/src/problem5
+docker compose up -d
+```
+
+What this does:
+
+- starts PostgreSQL in a container
+- exposes it on `127.0.0.1:5433`
+- creates the main DB `my_challenge`
+- creates the test DB `my_challenge_test`
+
+Check container status:
+
+```bash
+docker compose ps
+```
+
+Expected healthy output shape:
+
+```text
+NAME IMAGE STATUS PORTS
+my-challenge-problem5-postgres postgres:16-alpine Up (healthy) 0.0.0.0:5433->5432/tcp
+```
+
+### 3. Configure environment
+
+You can rely on the built-in defaults, or create a local `.env`.
+
+Example:
+
+```bash
+PORT=3000
+DATABASE_URL=postgres://app:app@127.0.0.1:5433/my_challenge
+TEST_DATABASE_URL=postgres://app:app@127.0.0.1:5433/my_challenge_test
+```
+
+There is also a ready-made example file:
+
+```bash
+cd my-challenge
+cp src/problem5/.env.example .env
+```
+
+## Running the Application
+
+From the repository root:
+
+```bash
+cd my-challenge
+npm run dev:problem5
+```
+
+Expected startup output:
+
+```text
+Problem 5 server listening on http://localhost:3000
+```
+
+## API Base URL
+
+All examples below use:
+
+```text
+http://localhost:3000
+```
+
+Resource routes live under:
+
+```text
+http://localhost:3000/resources
+```
+
+## API Endpoints
+
+### 1. Create a Resource
+
+- **Method**: `POST`
+- **URL**: `http://localhost:3000/resources`
+
+#### Example Request
+
+```bash
+curl -X POST http://localhost:3000/resources \
+ -H "Content-Type: application/json" \
+ -d '{
+ "code": "SKU-001",
+ "name": "Warehouse Item",
+ "description": "Primary stock item",
+ "quantity": 10
+ }'
+```
+
+#### Example Response `201 Created`
+
+```json
+{
+ "id": 1,
+ "code": "SKU-001",
+ "name": "Warehouse Item",
+ "description": "Primary stock item",
+ "quantity": 10,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:17:11.245Z"
+}
+```
+
+#### Example Validation Error `400 Bad Request`
+
+```json
+{
+ "error": "Field 'code' is required"
+}
+```
+
+#### Example Duplicate Code Error `409 Conflict`
+
+```json
+{
+ "error": "Resource code already exists"
+}
+```
+
+### 2. List Resources
+
+- **Method**: `GET`
+- **URL**: `http://localhost:3000/resources`
+
+#### Example Request: list all
+
+```bash
+curl http://localhost:3000/resources
+```
+
+#### Example Response `200 OK`
+
+```json
+[
+ {
+ "id": 1,
+ "code": "SKU-001",
+ "name": "Warehouse Item",
+ "description": "Primary stock item",
+ "quantity": 10,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:17:11.245Z"
+ },
+ {
+ "id": 2,
+ "code": "SKU-002",
+ "name": "Overflow Item",
+ "description": "Stored in secondary area",
+ "quantity": 3,
+ "createdAt": "2026-04-17T03:17:32.001Z",
+ "updatedAt": "2026-04-17T03:17:32.001Z"
+ }
+]
+```
+
+#### Example Request: filter by name
+
+```bash
+curl "http://localhost:3000/resources?name=Warehouse"
+```
+
+#### Example Request: filter by minimum quantity
+
+```bash
+curl "http://localhost:3000/resources?minQuantity=5"
+```
+
+#### Example Request: combined filters
+
+```bash
+curl "http://localhost:3000/resources?name=Item&minQuantity=5"
+```
+
+#### Example Validation Error `400 Bad Request`
+
+```json
+{
+ "error": "Query parameter 'minQuantity' must be an integer"
+}
+```
+
+### 3. Get Resource Details
+
+- **Method**: `GET`
+- **URL**: `http://localhost:3000/resources/:id`
+
+#### Example Request
+
+```bash
+curl http://localhost:3000/resources/1
+```
+
+#### Example Response `200 OK`
+
+```json
+{
+ "id": 1,
+ "code": "SKU-001",
+ "name": "Warehouse Item",
+ "description": "Primary stock item",
+ "quantity": 10,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:17:11.245Z"
+}
+```
+
+#### Example Not Found `404 Not Found`
+
+```json
+{
+ "error": "Resource not found"
+}
+```
+
+### 4. Update a Resource
+
+- **Method**: `PUT`
+- **URL**: `http://localhost:3000/resources/:id`
+
+Partial update is supported. You can update any combination of:
+
+- `code`
+- `name`
+- `description`
+- `quantity`
+
+#### Example Request
+
+```bash
+curl -X PUT http://localhost:3000/resources/1 \
+ -H "Content-Type: application/json" \
+ -d '{
+ "code": "SKU-001-A",
+ "name": "Warehouse Item - Updated",
+ "description": "Updated stock label",
+ "quantity": 25
+ }'
+```
+
+#### Example Response `200 OK`
+
+```json
+{
+ "id": 1,
+ "code": "SKU-001-A",
+ "name": "Warehouse Item - Updated",
+ "description": "Updated stock label",
+ "quantity": 25,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:20:42.552Z"
+}
+```
+
+#### Example Validation Error `400 Bad Request`
+
+```json
+{
+ "error": "At least one updatable field is required"
+}
+```
+
+#### Example Duplicate Code Error `409 Conflict`
+
+```json
+{
+ "error": "Resource code already exists"
+}
+```
+
+### 5. Increment Resource Quantity Safely
+
+- **Method**: `POST`
+- **URL**: `http://localhost:3000/resources/:id/increment`
+
+This endpoint exists to demonstrate database correctness under concurrency.
+
+Internally, the service:
+
+1. opens a transaction
+2. reads the target row with a pessimistic write lock
+3. calculates the new quantity while holding the lock
+4. writes the updated quantity
+5. commits the transaction
+
+In PostgreSQL, that maps to `SELECT ... FOR UPDATE`.
+
+#### Example Request
+
+```bash
+curl -X POST http://localhost:3000/resources/1/increment \
+ -H "Content-Type: application/json" \
+ -d '{
+ "amount": 5
+ }'
+```
+
+#### Example Response `200 OK`
+
+```json
+{
+ "id": 1,
+ "code": "SKU-001-A",
+ "name": "Warehouse Item - Updated",
+ "description": "Updated stock label",
+ "quantity": 30,
+ "createdAt": "2026-04-17T03:17:11.245Z",
+ "updatedAt": "2026-04-17T03:21:03.044Z"
+}
+```
+
+#### Example Validation Error `400 Bad Request`
+
+```json
+{
+ "error": "Field 'amount' must be a non-zero integer"
+}
+```
+
+#### Why this matters
+
+If 20 parallel requests increment the same row by `5`, the final value should be:
+
+```text
+initial_quantity + (20 * 5)
+```
+
+The integration test proves this behavior using a real PostgreSQL database.
+
+### 6. Delete a Resource
+
+- **Method**: `DELETE`
+- **URL**: `http://localhost:3000/resources/:id`
+
+#### Example Request
+
+```bash
+curl -X DELETE http://localhost:3000/resources/1
+```
+
+#### Example Response `200 OK`
+
+```json
+{
+ "message": "Resource deleted successfully"
+}
+```
+
+#### Example Not Found `404 Not Found`
+
+```json
+{
+ "error": "Resource not found"
+}
+```
+
+## End-to-End Example Flow
+
+### Create
+
+```bash
+curl -X POST http://localhost:3000/resources \
+ -H "Content-Type: application/json" \
+ -d '{
+ "code": "DEMO-001",
+ "name": "Demo Item",
+ "description": "Created during README walkthrough",
+ "quantity": 12
+ }'
+```
+
+### List
+
+```bash
+curl http://localhost:3000/resources
+```
+
+### Get by ID
+
+```bash
+curl http://localhost:3000/resources/1
+```
+
+### Update
+
+```bash
+curl -X PUT http://localhost:3000/resources/1 \
+ -H "Content-Type: application/json" \
+ -d '{
+ "quantity": 20
+ }'
+```
+
+### Increment with lock
+
+```bash
+curl -X POST http://localhost:3000/resources/1/increment \
+ -H "Content-Type: application/json" \
+ -d '{
+ "amount": 7
+ }'
+```
+
+### Delete
+
+```bash
+curl -X DELETE http://localhost:3000/resources/1
+```
+
+## Integration Testing
+
+The integration test suite uses:
+
+- a real PostgreSQL database
+- real HTTP requests against the Express app
+- setup hooks
+- cleanup hooks
+- a concurrency test for the row-locking path
+
+Run from the repository root:
+
+```bash
+cd my-challenge
+npm run test:problem5
+```
+
+### What the tests cover
+
+1. create and retrieve a resource
+2. run many parallel increment requests against the same row
+3. verify that the final quantity is exact and no update is lost
+
+## Troubleshooting
+
+### API cannot connect to PostgreSQL
+
+Check the Docker container:
+
+```bash
+cd my-challenge/src/problem5
+docker compose ps
+```
+
+### Port 3000 is already in use
+
+Set a different `PORT` in `.env`:
+
+```bash
+PORT=3001
+```
+
+### Port 5433 is already in use
+
+Change the host-side port mapping in `docker-compose.yml` and update `DATABASE_URL` / `TEST_DATABASE_URL`.
+
+### Node modules missing
+
+From the repository root:
+
+```bash
+npm install
+```
+
+## Notes
+
+- `synchronize: true` is enabled for speed in a coding exercise.
+- In production, I would replace that with migrations.
+- The Docker database setup makes the project reproducible on another machine.
+- The increment endpoint is beyond the minimum CRUD requirement, but it is included deliberately to showcase transaction handling and lost-update prevention in a senior backend interview.
diff --git a/src/problem5/docker-compose.yml b/src/problem5/docker-compose.yml
new file mode 100644
index 0000000000..d8d7b9a177
--- /dev/null
+++ b/src/problem5/docker-compose.yml
@@ -0,0 +1,22 @@
+services:
+ postgres:
+ image: postgres:16-alpine
+ container_name: my-challenge-problem5-postgres
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: app
+ POSTGRES_PASSWORD: app
+ POSTGRES_DB: my_challenge
+ ports:
+ - "5433:5432"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ - ./docker/init:/docker-entrypoint-initdb.d:ro
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U app -d my_challenge"]
+ interval: 5s
+ timeout: 5s
+ retries: 20
+
+volumes:
+ postgres_data:
diff --git a/src/problem5/docker/init/01-create-test-db.sql b/src/problem5/docker/init/01-create-test-db.sql
new file mode 100644
index 0000000000..2574e6a496
--- /dev/null
+++ b/src/problem5/docker/init/01-create-test-db.sql
@@ -0,0 +1 @@
+CREATE DATABASE my_challenge_test;
diff --git a/src/problem5/src/app.ts b/src/problem5/src/app.ts
new file mode 100644
index 0000000000..5ea685bfdf
--- /dev/null
+++ b/src/problem5/src/app.ts
@@ -0,0 +1,16 @@
+import express from "express";
+import cors from "cors";
+import { DataSource } from "typeorm";
+import { createResourceRouter } from "./routes/resource.routes";
+import { errorHandler } from "./middleware/error-handler";
+
+export function createApp(dataSource: DataSource) {
+ const app = express();
+
+ app.use(cors());
+ app.use(express.json());
+ app.use("/resources", createResourceRouter(dataSource));
+ app.use(errorHandler);
+
+ return app;
+}
diff --git a/src/problem5/src/config/env.ts b/src/problem5/src/config/env.ts
new file mode 100644
index 0000000000..1b46a7df55
--- /dev/null
+++ b/src/problem5/src/config/env.ts
@@ -0,0 +1,23 @@
+import dotenv from "dotenv";
+
+dotenv.config();
+
+const defaultDatabaseUrl = "postgres://app:app@127.0.0.1:5433/my_challenge";
+const defaultTestDatabaseUrl = "postgres://app:app@127.0.0.1:5433/my_challenge_test";
+
+function readEnv(name: string, fallback?: string): string {
+ const value = process.env[name] ?? fallback;
+
+ if (value === undefined) {
+ throw new Error(`Missing required environment variable: ${name}`);
+ }
+
+ return value;
+}
+
+export const env = {
+ nodeEnv: process.env.NODE_ENV ?? "development",
+ port: Number(process.env.PORT ?? 3000),
+ databaseUrl: readEnv("DATABASE_URL", defaultDatabaseUrl),
+ testDatabaseUrl: readEnv("TEST_DATABASE_URL", defaultTestDatabaseUrl)
+};
diff --git a/src/problem5/src/controllers/resource.controller.ts b/src/problem5/src/controllers/resource.controller.ts
new file mode 100644
index 0000000000..3c97f309dc
--- /dev/null
+++ b/src/problem5/src/controllers/resource.controller.ts
@@ -0,0 +1,64 @@
+import { Request, Response } from "express";
+import { ResourceService } from "../services/resource.service";
+import { HttpError } from "../utils/http-error";
+import {
+ validateCreateResourceInput,
+ validateIncrementResourceInput,
+ validateResourceId,
+ validateUpdateResourceInput
+} from "../validators/resource.validator";
+
+export class ResourceController {
+ public constructor(private readonly resourceService: ResourceService) {}
+
+ public createResource = async (req: Request, res: Response): Promise => {
+ const input = validateCreateResourceInput(req.body);
+ const resource = await this.resourceService.createResource(input);
+ res.status(201).json(resource);
+ };
+
+ public listResources = async (req: Request, res: Response): Promise => {
+ const minQuantityRaw = req.query.minQuantity;
+ let minQuantity: number | undefined;
+
+ if (typeof minQuantityRaw === "string" && minQuantityRaw.length > 0) {
+ minQuantity = Number(minQuantityRaw);
+ if (!Number.isInteger(minQuantity)) {
+ throw new HttpError(400, "Query parameter 'minQuantity' must be an integer");
+ }
+ }
+
+ const resources = await this.resourceService.listResources({
+ name: typeof req.query.name === "string" ? req.query.name : undefined,
+ minQuantity
+ });
+
+ res.status(200).json(resources);
+ };
+
+ public getResource = async (req: Request, res: Response): Promise => {
+ const resourceId = validateResourceId(req.params.id);
+ const resource = await this.resourceService.getResourceById(resourceId);
+ res.status(200).json(resource);
+ };
+
+ public updateResource = async (req: Request, res: Response): Promise => {
+ const resourceId = validateResourceId(req.params.id);
+ const input = validateUpdateResourceInput(req.body);
+ const resource = await this.resourceService.updateResource(resourceId, input);
+ res.status(200).json(resource);
+ };
+
+ public incrementResourceQuantity = async (req: Request, res: Response): Promise => {
+ const resourceId = validateResourceId(req.params.id);
+ const input = validateIncrementResourceInput(req.body);
+ const resource = await this.resourceService.incrementQuantityWithLock(resourceId, input);
+ res.status(200).json(resource);
+ };
+
+ public deleteResource = async (req: Request, res: Response): Promise => {
+ const resourceId = validateResourceId(req.params.id);
+ await this.resourceService.deleteResource(resourceId);
+ res.status(200).json({ message: "Resource deleted successfully" });
+ };
+}
diff --git a/src/problem5/src/db/data-source.ts b/src/problem5/src/db/data-source.ts
new file mode 100644
index 0000000000..fb962e361f
--- /dev/null
+++ b/src/problem5/src/db/data-source.ts
@@ -0,0 +1,14 @@
+import "reflect-metadata";
+import { DataSource } from "typeorm";
+import { env } from "../config/env";
+import { ResourceEntity } from "../entities/resource.entity";
+
+export function createAppDataSource(databaseUrl = env.databaseUrl): DataSource {
+ return new DataSource({
+ type: "postgres",
+ url: databaseUrl,
+ entities: [ResourceEntity],
+ synchronize: true,
+ logging: false
+ });
+}
diff --git a/src/problem5/src/entities/resource.entity.ts b/src/problem5/src/entities/resource.entity.ts
new file mode 100644
index 0000000000..f63963fde0
--- /dev/null
+++ b/src/problem5/src/entities/resource.entity.ts
@@ -0,0 +1,33 @@
+import {
+ Column,
+ CreateDateColumn,
+ Entity,
+ Index,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn
+} from "typeorm";
+
+@Entity({ name: "resources" })
+export class ResourceEntity {
+ @PrimaryGeneratedColumn()
+ id!: number;
+
+ @Index("uq_resources_code", { unique: true })
+ @Column({ type: "varchar", length: 64 })
+ code!: string;
+
+ @Column({ type: "varchar", length: 120 })
+ name!: string;
+
+ @Column({ type: "varchar", length: 500, nullable: true })
+ description!: string | null;
+
+ @Column({ type: "integer", default: 0 })
+ quantity!: number;
+
+ @CreateDateColumn({ name: "created_at" })
+ createdAt!: Date;
+
+ @UpdateDateColumn({ name: "updated_at" })
+ updatedAt!: Date;
+}
diff --git a/src/problem5/src/middleware/async-handler.ts b/src/problem5/src/middleware/async-handler.ts
new file mode 100644
index 0000000000..b5a7d5af08
--- /dev/null
+++ b/src/problem5/src/middleware/async-handler.ts
@@ -0,0 +1,9 @@
+import { NextFunction, Request, RequestHandler, Response } from "express";
+
+type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise;
+
+export function asyncHandler(handler: AsyncHandler): RequestHandler {
+ return (req, res, next) => {
+ void handler(req, res, next).catch(next);
+ };
+}
diff --git a/src/problem5/src/middleware/error-handler.ts b/src/problem5/src/middleware/error-handler.ts
new file mode 100644
index 0000000000..f123a09b69
--- /dev/null
+++ b/src/problem5/src/middleware/error-handler.ts
@@ -0,0 +1,17 @@
+import { NextFunction, Request, Response } from "express";
+import { HttpError } from "../utils/http-error";
+
+export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction): void {
+ if (err instanceof HttpError) {
+ res.status(err.statusCode).json({
+ error: err.message,
+ details: err.details
+ });
+ return;
+ }
+
+ console.error(err);
+ res.status(500).json({
+ error: "Internal server error"
+ });
+}
diff --git a/src/problem5/src/repositories/resource.repository.ts b/src/problem5/src/repositories/resource.repository.ts
new file mode 100644
index 0000000000..48b868ed60
--- /dev/null
+++ b/src/problem5/src/repositories/resource.repository.ts
@@ -0,0 +1,44 @@
+import { DataSource, Repository } from "typeorm";
+import { ResourceEntity } from "../entities/resource.entity";
+
+export class ResourceRepository {
+ private readonly repository: Repository;
+
+ public constructor(dataSource: DataSource) {
+ this.repository = dataSource.getRepository(ResourceEntity);
+ }
+
+ public create(data: Partial): ResourceEntity {
+ return this.repository.create(data);
+ }
+
+ public save(resource: ResourceEntity): Promise {
+ return this.repository.save(resource);
+ }
+
+ public findById(id: number): Promise {
+ return this.repository.findOne({ where: { id } });
+ }
+
+ public findByCode(code: string): Promise {
+ return this.repository.findOne({ where: { code } });
+ }
+
+ public findWithFilters(filters: { readonly name?: string; readonly minQuantity?: number }): Promise {
+ const queryBuilder = this.repository.createQueryBuilder("resource");
+
+ if (filters.name) {
+ queryBuilder.andWhere("resource.name ILIKE :name", { name: `%${filters.name}%` });
+ }
+
+ if (filters.minQuantity !== undefined) {
+ queryBuilder.andWhere("resource.quantity >= :minQuantity", { minQuantity: filters.minQuantity });
+ }
+
+ return queryBuilder.orderBy("resource.id", "ASC").getMany();
+ }
+
+ public remove(resource: ResourceEntity): Promise {
+ return this.repository.remove(resource);
+ }
+}
diff --git a/src/problem5/src/routes/resource.routes.ts b/src/problem5/src/routes/resource.routes.ts
new file mode 100644
index 0000000000..f8fe732064
--- /dev/null
+++ b/src/problem5/src/routes/resource.routes.ts
@@ -0,0 +1,19 @@
+import { Router } from "express";
+import { DataSource } from "typeorm";
+import { ResourceController } from "../controllers/resource.controller";
+import { asyncHandler } from "../middleware/async-handler";
+import { ResourceService } from "../services/resource.service";
+
+export function createResourceRouter(dataSource: DataSource): Router {
+ const router = Router();
+ const resourceController = new ResourceController(new ResourceService(dataSource));
+
+ router.post("/", asyncHandler(resourceController.createResource));
+ router.get("/", asyncHandler(resourceController.listResources));
+ router.get("/:id", asyncHandler(resourceController.getResource));
+ router.put("/:id", asyncHandler(resourceController.updateResource));
+ router.post("/:id/increment", asyncHandler(resourceController.incrementResourceQuantity));
+ router.delete("/:id", asyncHandler(resourceController.deleteResource));
+
+ return router;
+}
diff --git a/src/problem5/src/server.ts b/src/problem5/src/server.ts
new file mode 100644
index 0000000000..c406bb1349
--- /dev/null
+++ b/src/problem5/src/server.ts
@@ -0,0 +1,21 @@
+import { createServer } from "node:http";
+import { env } from "./config/env";
+import { createApp } from "./app";
+import { createAppDataSource } from "./db/data-source";
+
+async function bootstrap(): Promise {
+ const dataSource = createAppDataSource();
+ await dataSource.initialize();
+
+ const app = createApp(dataSource);
+ const server = createServer(app);
+
+ server.listen(env.port, () => {
+ console.log(`Problem 5 server listening on http://localhost:${env.port}`);
+ });
+}
+
+void bootstrap().catch((error) => {
+ console.error("Failed to start Problem 5 server", error);
+ process.exitCode = 1;
+});
diff --git a/src/problem5/src/services/resource.service.ts b/src/problem5/src/services/resource.service.ts
new file mode 100644
index 0000000000..e3f90a6e72
--- /dev/null
+++ b/src/problem5/src/services/resource.service.ts
@@ -0,0 +1,125 @@
+import { DataSource, QueryFailedError } from "typeorm";
+import { ResourceEntity } from "../entities/resource.entity";
+import { ResourceRepository } from "../repositories/resource.repository";
+import { CreateResourceInput, IncrementResourceInput, UpdateResourceInput } from "../types/resource";
+import { HttpError } from "../utils/http-error";
+import { sleep } from "../utils/sleep";
+
+export class ResourceService {
+ private readonly repository: ResourceRepository;
+
+ public constructor(private readonly dataSource: DataSource) {
+ this.repository = new ResourceRepository(dataSource);
+ }
+
+ public async createResource(input: CreateResourceInput): Promise {
+ const normalizedCode = ResourceService.normalizeCode(input.code);
+ await this.ensureCodeIsAvailable(normalizedCode);
+
+ try {
+ const resource = this.repository.create({
+ code: normalizedCode,
+ name: input.name,
+ description: input.description ?? null,
+ quantity: input.quantity ?? 0
+ });
+
+ return await this.repository.save(resource);
+ } catch (error) {
+ throw ResourceService.mapPersistenceError(error);
+ }
+ }
+
+ public listResources(filters: { readonly name?: string; readonly minQuantity?: number }): Promise {
+ return this.repository.findWithFilters(filters);
+ }
+
+ public async getResourceById(id: number): Promise {
+ const resource = await this.repository.findById(id);
+
+ if (!resource) {
+ throw new HttpError(404, "Resource not found");
+ }
+
+ return resource;
+ }
+
+ public async updateResource(id: number, input: UpdateResourceInput): Promise {
+ const resource = await this.getResourceById(id);
+ console.log("🚀 ~ ResourceService ~ updateResource ~ resource:", resource)
+
+ if (input.code !== undefined) {
+ const normalizedCode = ResourceService.normalizeCode(input.code);
+ await this.ensureCodeIsAvailable(normalizedCode, resource.id);
+ resource.code = normalizedCode;
+ }
+
+ if (input.name !== undefined) {
+ resource.name = input.name;
+ }
+
+ if (input.description !== undefined) {
+ resource.description = input.description;
+ }
+
+ if (input.quantity !== undefined) {
+ resource.quantity = input.quantity;
+ }
+
+ try {
+ return await this.repository.save(resource);
+ } catch (error) {
+ throw ResourceService.mapPersistenceError(error);
+ }
+ }
+
+ public async deleteResource(id: number): Promise {
+ const resource = await this.getResourceById(id);
+ await this.repository.remove(resource);
+ }
+
+ public async incrementQuantityWithLock(id: number, input: IncrementResourceInput): Promise {
+ return this.dataSource.transaction(async (transactionManager) => {
+ const repository = transactionManager.getRepository(ResourceEntity);
+ const resource = await repository
+ .createQueryBuilder("resource")
+ .setLock("pessimistic_write")
+ .where("resource.id = :id", { id })
+ .getOne();
+
+ if (!resource) {
+ throw new HttpError(404, "Resource not found");
+ }
+
+ if (input.simulateDelayMs && input.simulateDelayMs > 0) {
+ await sleep(input.simulateDelayMs);
+ }
+
+ resource.quantity += input.amount;
+ return repository.save(resource);
+ });
+ }
+
+ private static normalizeCode(code: string): string {
+ return code.trim().toUpperCase();
+ }
+
+ private async ensureCodeIsAvailable(code: string, currentResourceId?: number): Promise {
+ const existingResource = await this.repository.findByCode(code);
+
+ if (existingResource && existingResource.id !== currentResourceId) {
+ throw new HttpError(409, "Resource code already exists");
+ }
+ }
+
+ private static mapPersistenceError(error: unknown): Error {
+ if (error instanceof QueryFailedError) {
+ const driverError = error.driverError as { code?: string } | undefined;
+ if (driverError?.code === "23505") {
+ return new HttpError(409, "Resource code already exists");
+ }
+ }
+
+ return error instanceof Error ? error : new Error("Unknown persistence error");
+ }
+}
diff --git a/src/problem5/src/types/resource.ts b/src/problem5/src/types/resource.ts
new file mode 100644
index 0000000000..ddf51c3f1e
--- /dev/null
+++ b/src/problem5/src/types/resource.ts
@@ -0,0 +1,18 @@
+export type CreateResourceInput = {
+ code: string;
+ name: string;
+ description?: string;
+ quantity?: number;
+};
+
+export type UpdateResourceInput = {
+ code?: string;
+ name?: string;
+ description?: string | null;
+ quantity?: number;
+};
+
+export type IncrementResourceInput = {
+ amount: number;
+ simulateDelayMs?: number;
+};
diff --git a/src/problem5/src/utils/http-error.ts b/src/problem5/src/utils/http-error.ts
new file mode 100644
index 0000000000..9b5e506457
--- /dev/null
+++ b/src/problem5/src/utils/http-error.ts
@@ -0,0 +1,10 @@
+export class HttpError extends Error {
+ public readonly statusCode: number;
+ public readonly details?: unknown;
+
+ public constructor(statusCode: number, message: string, details?: unknown) {
+ super(message);
+ this.statusCode = statusCode;
+ this.details = details;
+ }
+}
diff --git a/src/problem5/src/utils/sleep.ts b/src/problem5/src/utils/sleep.ts
new file mode 100644
index 0000000000..df0d03d81c
--- /dev/null
+++ b/src/problem5/src/utils/sleep.ts
@@ -0,0 +1,5 @@
+export function sleep(milliseconds: number): Promise {
+ return new Promise((resolve) => {
+ setTimeout(resolve, milliseconds);
+ });
+}
diff --git a/src/problem5/src/validators/resource.validator.ts b/src/problem5/src/validators/resource.validator.ts
new file mode 100644
index 0000000000..96f2793fa2
--- /dev/null
+++ b/src/problem5/src/validators/resource.validator.ts
@@ -0,0 +1,134 @@
+import { HttpError } from "../utils/http-error";
+import { CreateResourceInput, IncrementResourceInput, UpdateResourceInput } from "../types/resource";
+
+function asTrimmedString(value: unknown): string | undefined {
+ if (typeof value !== "string") {
+ return undefined;
+ }
+
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : undefined;
+}
+
+function asInteger(value: unknown): number | undefined {
+ if (typeof value !== "number" || !Number.isInteger(value)) {
+ return undefined;
+ }
+
+ return value;
+}
+
+export function validateResourceId(rawId: string): number {
+ const id = Number(rawId);
+
+ if (!Number.isInteger(id) || id < 1) {
+ throw new HttpError(400, "Resource id must be a positive integer");
+ }
+
+ return id;
+}
+
+export function validateCreateResourceInput(body: unknown): CreateResourceInput {
+ if (body === null || typeof body !== "object") {
+ throw new HttpError(400, "Request body must be an object");
+ }
+
+ const payload = body as Record;
+ const code = asTrimmedString(payload.code);
+ const name = asTrimmedString(payload.name);
+ const description = payload.description === undefined ? undefined : asTrimmedString(payload.description);
+ const quantity = payload.quantity === undefined ? undefined : asInteger(payload.quantity);
+
+ if (!code) {
+ throw new HttpError(400, "Field 'code' is required");
+ }
+
+ if (!name) {
+ throw new HttpError(400, "Field 'name' is required");
+ }
+
+ if (payload.quantity !== undefined && quantity === undefined) {
+ throw new HttpError(400, "Field 'quantity' must be an integer");
+ }
+
+ return {
+ code,
+ name,
+ description,
+ quantity
+ };
+}
+
+export function validateUpdateResourceInput(body: unknown): UpdateResourceInput {
+ if (body === null || typeof body !== "object") {
+ throw new HttpError(400, "Request body must be an object");
+ }
+
+ const payload = body as Record;
+ const result: UpdateResourceInput = {};
+
+ if (payload.code !== undefined) {
+ const code = asTrimmedString(payload.code);
+ if (!code) {
+ throw new HttpError(400, "Field 'code' must be a non-empty string");
+ }
+ result.code = code;
+ }
+
+ if (payload.name !== undefined) {
+ const name = asTrimmedString(payload.name);
+ if (!name) {
+ throw new HttpError(400, "Field 'name' must be a non-empty string");
+ }
+ result.name = name;
+ }
+
+ if (payload.description !== undefined) {
+ if (payload.description === null) {
+ result.description = null;
+ } else {
+ const description = asTrimmedString(payload.description);
+ if (!description) {
+ throw new HttpError(400, "Field 'description' must be a non-empty string or null");
+ }
+ result.description = description;
+ }
+ }
+
+ if (payload.quantity !== undefined) {
+ const quantity = asInteger(payload.quantity);
+ if (quantity === undefined) {
+ throw new HttpError(400, "Field 'quantity' must be an integer");
+ }
+ result.quantity = quantity;
+ }
+
+ if (Object.keys(result).length === 0) {
+ throw new HttpError(400, "At least one updatable field is required");
+ }
+
+ return result;
+}
+
+export function validateIncrementResourceInput(body: unknown): IncrementResourceInput {
+ if (body === null || typeof body !== "object") {
+ throw new HttpError(400, "Request body must be an object");
+ }
+
+ const payload = body as Record;
+ const amount = asInteger(payload.amount);
+
+ if (amount === undefined || amount === 0) {
+ throw new HttpError(400, "Field 'amount' must be a non-zero integer");
+ }
+
+ let simulateDelayMs: number | undefined;
+ if (payload.simulateDelayMs !== undefined) {
+ simulateDelayMs = asInteger(payload.simulateDelayMs);
+ if (simulateDelayMs === undefined || simulateDelayMs < 0) {
+ throw new HttpError(400, "Field 'simulateDelayMs' must be a non-negative integer");
+ }
+ }
+
+ return { amount, simulateDelayMs };
+}
diff --git a/src/problem5/tests/integration/resource.integration.test.ts b/src/problem5/tests/integration/resource.integration.test.ts
new file mode 100644
index 0000000000..80fec6793c
--- /dev/null
+++ b/src/problem5/tests/integration/resource.integration.test.ts
@@ -0,0 +1,181 @@
+import test, { after, afterEach, before } from "node:test";
+import assert from "node:assert/strict";
+import { AddressInfo } from "node:net";
+import { Server } from "node:http";
+import { DataSource } from "typeorm";
+import { createApp } from "../../src/app";
+import { env } from "../../src/config/env";
+import { createAppDataSource } from "../../src/db/data-source";
+import { ResourceEntity } from "../../src/entities/resource.entity";
+
+let dataSource: DataSource;
+let server: Server;
+let baseUrl: string;
+
+before(async () => {
+ dataSource = createAppDataSource(env.testDatabaseUrl);
+ await dataSource.initialize();
+
+ const app = createApp(dataSource);
+ server = app.listen(0);
+
+ await new Promise((resolve) => {
+ server.once("listening", () => resolve());
+ });
+
+ const address = server.address() as AddressInfo;
+ baseUrl = `http://127.0.0.1:${address.port}`;
+});
+
+afterEach(async () => {
+ await dataSource.query("TRUNCATE TABLE resources RESTART IDENTITY CASCADE");
+});
+
+after(async () => {
+ await new Promise((resolve, reject) => {
+ server.close((error) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+
+ resolve();
+ });
+ });
+
+ await dataSource.destroy();
+});
+
+test("creates and retrieves a resource", { concurrency: false }, async () => {
+ const createResponse = await fetch(`${baseUrl}/resources`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "sku-001",
+ name: "Warehouse item",
+ description: "Created from integration test",
+ quantity: 10
+ })
+ });
+
+ assert.equal(createResponse.status, 201);
+ const createdResource = (await createResponse.json()) as ResourceEntity;
+ assert.equal(createdResource.code, "SKU-001");
+ assert.equal(createdResource.name, "Warehouse item");
+ assert.equal(createdResource.quantity, 10);
+
+ const getResponse = await fetch(`${baseUrl}/resources/${createdResource.id}`);
+ assert.equal(getResponse.status, 200);
+
+ const fetchedResource = (await getResponse.json()) as ResourceEntity;
+ assert.equal(fetchedResource.id, createdResource.id);
+ assert.equal(fetchedResource.code, "SKU-001");
+ assert.equal(fetchedResource.description, "Created from integration test");
+});
+
+test("rejects duplicate resource code on create", { concurrency: false }, async () => {
+ const firstCreateResponse = await fetch(`${baseUrl}/resources`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "SKU-001",
+ name: "First item",
+ quantity: 10
+ })
+ });
+
+ assert.equal(firstCreateResponse.status, 201);
+
+ const duplicateCreateResponse = await fetch(`${baseUrl}/resources`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "sku-001",
+ name: "Duplicate item",
+ quantity: 5
+ })
+ });
+
+ assert.equal(duplicateCreateResponse.status, 409);
+ const payload = (await duplicateCreateResponse.json()) as { error: string };
+ assert.equal(payload.error, "Resource code already exists");
+});
+
+test("rejects duplicate resource code on update", { concurrency: false }, async () => {
+ const firstCreateResponse = await fetch(`${baseUrl}/resources`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "SKU-001",
+ name: "First item",
+ quantity: 10
+ })
+ });
+ assert.equal(firstCreateResponse.status, 201);
+ const firstResource = (await firstCreateResponse.json()) as ResourceEntity;
+
+ const secondCreateResponse = await fetch(`${baseUrl}/resources`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "SKU-002",
+ name: "Second item",
+ quantity: 4
+ })
+ });
+ assert.equal(secondCreateResponse.status, 201);
+ const secondResource = (await secondCreateResponse.json()) as ResourceEntity;
+
+ const updateResponse = await fetch(`${baseUrl}/resources/${secondResource.id}`, {
+ method: "PUT",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ code: "sku-001"
+ })
+ });
+
+ assert.equal(updateResponse.status, 409);
+ const payload = (await updateResponse.json()) as { error: string };
+ assert.equal(payload.error, "Resource code already exists");
+
+ const repository = dataSource.getRepository(ResourceEntity);
+ const reloadedFirst = await repository.findOneByOrFail({ id: firstResource.id });
+ const reloadedSecond = await repository.findOneByOrFail({ id: secondResource.id });
+ assert.equal(reloadedFirst.code, "SKU-001");
+ assert.equal(reloadedSecond.code, "SKU-002");
+});
+
+test("prevents lost updates under parallel increment requests using a pessimistic row lock", { concurrency: false }, async () => {
+ const repository = dataSource.getRepository(ResourceEntity);
+ const resource = await repository.save(
+ repository.create({
+ code: "SKU-LOCK-001",
+ name: "Concurrent counter",
+ description: "Used to verify SELECT ... FOR UPDATE behavior",
+ quantity: 0
+ })
+ );
+
+ const requestCount = 20;
+ const incrementAmount = 5;
+
+ const responses = await Promise.all(
+ Array.from({ length: requestCount }, async () =>
+ fetch(`${baseUrl}/resources/${resource.id}/increment`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ amount: incrementAmount,
+ simulateDelayMs: 50
+ })
+ })
+ )
+ );
+
+ for (const response of responses) {
+ assert.equal(response.status, 200);
+ }
+
+ const reloaded = await repository.findOneByOrFail({ id: resource.id });
+ assert.equal(reloaded.quantity, requestCount * incrementAmount);
+});
diff --git a/src/problem6/ARCHITECTURE.md b/src/problem6/ARCHITECTURE.md
new file mode 100644
index 0000000000..332f8533e9
--- /dev/null
+++ b/src/problem6/ARCHITECTURE.md
@@ -0,0 +1,1128 @@
+# Problem 6: Architecture & Execution Flows
+
+This document is the detailed visual companion to `README.md`.
+
+The goal is to make the system implementation concrete enough for a backend team to build without needing to infer the critical write-path, projection, fraud, and recovery behavior.
+
+Unlike the passed example, this design does not update PostgreSQL, Redis, and realtime sockets in one synchronous chain. PostgreSQL remains authoritative, and downstream state is projected asynchronously through outbox + CDC + Kafka.
+
+This revision also removes the synchronous write-side total row. The authoritative write path is append-only:
+
+- accepted score events are written to PostgreSQL
+- Redis computes the live query model
+- an asynchronous PostgreSQL materialization worker can persist durable fallback totals outside the write transaction
+
+---
+
+## 1. High-Level System Architecture
+
+### 1.1 Topology at a glance
+
+```text
+Clients
+ |
+ v
+┌────────────────────────────────────────────────────────────┐
+│ API Gateway / Load Balancer │
+│ - TLS termination │
+│ - request routing │
+│ - coarse rate limiting │
+│ - auth pre-check hooks │
+└───────────────┬──────────────────────┬────────────────────┘
+ │ │
+ │ HTTP │ WSS
+ ▼ ▼
+ ┌──────────────────────┐ ┌──────────────────────┐
+ │ score-command-api │ │ socket gateway │
+ │ - auth + validation │ │ - connection auth │
+ │ - append score event │ │ - channel fanout │
+ │ - write outbox in tx │ └──────────▲───────────┘
+ └──────────┬───────────┘ │
+ │ │ leaderboard:initial
+ ▼ │ leaderboard:update
+ ┌──────────────────────┐ │
+ │ PostgreSQL │ │
+ │ - users / sessions │ │
+ │ - requests / events │ │
+ │ - outbox / flags │ │
+ │ - materialized totals│ │
+ └──────────┬───────────┘ │
+ │ committed outbox rows │
+ ▼ │
+ ┌──────────────────────┐ │
+ │ CDC Connector │ │
+ │ Debezium / similar │ │
+ │ - reads committed │ │
+ │ outbox rows │ │
+ └──────────┬───────────┘ │
+ ▼ │
+ ┌──────────────────────┐ │
+ │ Kafka │──────────────┼──────────────────────┐
+ │ - score-events topic │ │ │
+ │ - replay source │ │ │
+ │ - per-user ordering │ │ │
+ └───────┬────────┬─────┘ │ │
+ │ │ │ │
+ ▼ ▼ │ │
+ ┌──────────────────┐ ┌──────────────────────┐ │
+ │ score-projection │ │ score-materialization│ │
+ │ service / query │ │ worker │ │
+ │ facade │ │ - async PG totals │ │
+ │ - Kafka consumer │ │ - replay checkpoint │ │
+ │ - read APIs │ └──────────┬───────────┘ │
+ │ - Redis updater │ │ writes fallback state │
+ │ - fanout decision│─────────────┘ into PostgreSQL │
+ └──────────┬───────┘ │
+ │ read / write │
+ ▼ │
+ ┌──────────────────────┐ │
+ │ Redis │─────────────────────────────────────┘
+ │ - leaderboard │ visible leaderboard updates only
+ │ - user projections │
+ │ - fraud windows │
+ └──────────────────────┘
+```
+
+```text
+Clients
+ |
+ v
+API Gateway / Load Balancer
+ |
+ +--> POST /v1/actions/complete
+ | |
+ | v
+ | score-command-api
+ | |
+ | +--> authenticate + validate
+ | +--> append score_events
+ | +--> write outbox_events in same tx
+ | |
+ | v
+ | PostgreSQL
+ | |
+ | v
+ | CDC Connector
+ | |
+ | v
+ | Kafka score-events
+ | |
+ | +--> score-projection-service / query facade
+ | | |
+ | | +--> update Redis leaderboard + user projections + fraud windows
+ | | +--> decide leaderboard fanout
+ | | +--> push leaderboard:* to socket gateway
+ | |
+ | +--> score-materialization-worker
+ | |
+ | +--> upsert async PostgreSQL materialized totals
+ |
+ +--> GET /v1/leaderboard/top10
+ | GET /v1/users/:userId/score
+ | |
+ | v
+ | score-projection-service / query facade
+ | |
+ | +--> read Redis live projections
+ | +--> optional degraded fallback from PostgreSQL materialized totals
+ |
+ +--> WSS /leaderboard/live
+ |
+ v
+ socket gateway
+ |
+ +--> authenticate connection
+ +--> deliver leaderboard:initial / leaderboard:update
+```
+
+Current system truth:
+
+- `POST /v1/actions/complete` goes through `score-command-api` and commits only to PostgreSQL in the write transaction.
+- PostgreSQL outbox rows are streamed by CDC into Kafka after commit.
+- Kafka feeds both the Redis projection path and the asynchronous PostgreSQL materialization path.
+- `GET /v1/leaderboard/top10` and `GET /v1/users/:userId/score` are served from the projection/query side backed by Redis.
+- WebSocket clients connect to `socket gateway`, which receives `leaderboard:*` fanout from the projection service, not directly from the write transaction.
+
+### 1.2 Mermaid view
+
+```mermaid
+flowchart LR
+ Client[Web / Mobile Client]
+ Gateway[API Gateway / Load Balancer]
+ Command[score-command-api]
+ Query[score-projection-service / query facade]
+ Materialize[score-materialization-worker]
+ Socket[Socket Gateway]
+ PG[(PostgreSQL)]
+ CDC[CDC Connector\nDebezium]
+ Kafka[(Kafka)]
+ Redis[(Redis)]
+
+ Client -->|POST /v1/actions/complete| Gateway
+ Client -->|GET /v1/leaderboard/top10| Gateway
+ Client -->|GET /v1/users/:userId/score| Gateway
+ Client -->|WSS /leaderboard/live| Gateway
+
+ Gateway --> Command
+ Gateway --> Query
+ Gateway --> Socket
+
+ Command -->|transaction: requests + events + outbox| PG
+ PG -->|committed outbox rows| CDC
+ CDC -->|score-events| Kafka
+ Kafka --> Query
+ Kafka --> Materialize
+ Query --> Redis
+ Query --> Socket
+ Materialize -->|async durable totals| PG
+ Query -->|optional replay reads| PG
+```
+
+### 1.2.1 ASCII alternative
+
+```text
+Clients
+ |
+ v
+API Gateway / Load Balancer
+ |
+ +--> POST /v1/actions/complete --> score-command-api --> PostgreSQL --> CDC Connector --> Kafka
+ |
+ +--> GET /v1/leaderboard/top10 --> score-projection-service / query facade --> Redis
+ |
+ +--> GET /v1/users/:userId/score --> score-projection-service / query facade --> Redis
+ |
+ +--> WSS /leaderboard/live --> Socket Gateway
+
+Kafka --> score-projection-service / query facade --> Redis
+Kafka --> score-projection-service / query facade --> Socket Gateway
+Kafka --> score-materialization-worker --> PostgreSQL durable totals
+```
+
+### 1.3 Component responsibilities
+
+| Component | Primary responsibility | Must not own |
+| ------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------ |
+| API Gateway | routing, TLS, coarse throttling | authoritative score mutation |
+| `score-command-api` | auth, action validation, transactional score writes, outbox writes | direct Redis leaderboard truth |
+| PostgreSQL | source of truth for accepted writes and durable fallback state | low-latency top-10 serving |
+| CDC connector | move committed outbox rows into Kafka | business validation |
+| Kafka | durable event transport and replay | authoritative score state |
+| `score-projection-service` | consume events, update Redis, serve read APIs or power thin query layer | deciding whether a score mutation is valid |
+| `score-materialization-worker` | persist durable PostgreSQL materialized totals asynchronously | synchronous write-path correctness |
+| Redis | low-latency ranking and rolling fraud windows | authoritative score record |
+| Socket gateway | fanout to subscribed clients | transaction processing |
+
+### 1.4 Why the topology is split this way
+
+- The command side is optimized for correctness.
+- The read side is optimized for speed and fanout.
+- Kafka sits between them so failures on the read side do not invalidate accepted writes.
+- Redis is intentionally disposable. If it is lost, the system should rebuild it.
+- Durable PostgreSQL materialized totals are optional asynchronous helpers, not the source of truth.
+
+---
+
+## 2. Detailed Score Update Flow
+
+### 2.1 Scenario
+
+User completes an action in the product. The client calls `POST /v1/actions/complete`. The system must:
+
+- verify the user is allowed to claim that action
+- reject duplicates or replays
+- update the authoritative score exactly once from the user’s perspective
+- publish the resulting score change downstream without dual-write risk
+
+### 2.2 Step-by-step execution flow
+
+```text
+TIME │ COMPONENT │ ACTION
+─────┼──────────────────────────────┼────────────────────────────────────────────────────────────
+ 1 │ Client │ User completes action
+ │ │ Send POST /v1/actions/complete
+ │ │ Body: { actionId, idempotencyKey, actionInstanceId, ... }
+ │
+ 2 │ API Gateway │ Apply coarse request throttling / bot filtering
+ │ │ Forward request to score-command-api
+ │
+ 3 │ Auth Layer │ Validate access token
+ │ │ Resolve authenticated userId
+ │ │ Optionally load session / refresh state from PostgreSQL
+ │ │ Reject with 401 if invalid
+ │
+ 4 │ Request Validation │ Validate schema
+ │ │ Validate idempotencyKey presence and length
+ │ │ Validate actionId is supported
+ │ │ Reject with 400 on malformed input
+ │
+ 5 │ Business Eligibility │ Load action metadata
+ │ │ Check action is enabled
+ │ │ Check caller may claim it
+ │ │ Reject with 403 / 422 when not allowed
+ │
+ 6 │ PostgreSQL Transaction Start │ BEGIN
+ │ │ Start the authoritative mutation boundary
+ │
+ 7 │ Idempotency Insert │ INSERT score_action_requests(...)
+ │ │ Unique key: (user_id, idempotency_key)
+ │ │ If duplicate exists, return saved prior response
+ │
+ 8 │ Business/Fraud Checks │ Evaluate lightweight checks using request metadata
+ │ │ Example: invalid grant, impossible repetition, suspicious burst
+ │ │ No total-score row is locked here
+ │
+ 9 │ Ledger Append │ INSERT immutable row into score_events
+ │ │ Return generated event_id from PostgreSQL
+ │
+ 10 │ Outbox Write │ INSERT outbox_events(payload...)
+ │ │ Same transaction as the score event
+ │
+ 11 │ Fraud Flag Persistence │ INSERT fraud_flags if the event is suspicious enough
+ │ │ This does not block the scoreboard path unless policy says so
+ │
+ 12 │ Idempotent Response Save │ Save the accepted response body onto score_action_requests
+ │ │ So duplicate retries can return the original result verbatim
+ │
+ 13 │ Commit │ COMMIT
+ │ │ Score event is now durably accepted in PostgreSQL
+ │
+ 14 │ Response to Client │ Return acceptedEventId + scoreDelta
+ │ │ The projected total may appear slightly later
+ │
+ 15 │ CDC Connector │ Detect committed outbox row
+ │ │ Publish event into Kafka
+ │
+ 16 │ Projection Service │ Consume score event
+ │ │ Apply ZINCRBY / HINCRBY style projection in Redis
+ │
+ 17 │ Materialization Worker │ Optionally persist durable PostgreSQL totals asynchronously
+ │
+ 18 │ Socket Gateway │ Broadcast coalesced leaderboard:update if visible state changed
+ │
+ 19 │ Clients │ Re-render leaderboard / user score in realtime
+```
+
+### 2.3 Write-flow Mermaid sequence
+
+```mermaid
+sequenceDiagram
+ participant C as Client
+ participant G as API Gateway
+ participant A as score-command-api
+ participant P as PostgreSQL
+ participant D as CDC Connector
+ participant K as Kafka
+ participant S as score-projection-service
+ participant M as score-materialization-worker
+ participant R as Redis
+ participant W as Socket Gateway
+
+ C->>G: POST /v1/actions/complete
+ G->>A: Forward request
+ A->>A: Authenticate + validate + authorize
+ A->>P: BEGIN
+ A->>P: INSERT score_action_requests
+ alt duplicate idempotency key
+ P-->>A: existing request row
+ A->>P: ROLLBACK
+ A-->>C: Return original saved response
+ else new request
+ A->>P: INSERT score_events
+ A->>P: INSERT outbox_events
+ opt suspicious event
+ A->>P: INSERT fraud_flags
+ end
+ A->>P: UPDATE score_action_requests response_body
+ A->>P: COMMIT
+ A-->>C: Accepted score response
+ P-->>D: Committed outbox row visible
+ D->>K: Publish score.user_score_updated.v1
+ K->>S: Deliver event
+ opt durable fallback path
+ K->>M: Deliver event
+ M->>P: Upsert async materialized totals
+ end
+ S->>R: Increment leaderboard + user projection
+ opt visible leaderboard change
+ S->>W: leaderboard:update
+ W-->>C: Push realtime update
+ end
+ end
+```
+
+### 2.3.1 ASCII alternative
+
+```text
+Client
+ |
+ v
+API Gateway
+ |
+ v
+score-command-api
+ |
+ +--> authenticate + validate + authorize
+ |
+ +--> PostgreSQL BEGIN
+ |
+ +--> INSERT score_action_requests
+ |
+ +--> duplicate idempotency key?
+ |
+ +--> yes: read stored response -> ROLLBACK -> return original result
+ |
+ +--> no:
+ |
+ +--> INSERT score_events (append-only)
+ +--> INSERT outbox_events
+ +--> optionally INSERT fraud_flags
+ +--> save response body on request row
+ +--> COMMIT
+ |
+ +--> return acceptedEventId + scoreDelta to client
+
+After commit:
+PostgreSQL -> CDC Connector -> Kafka -> projection service -> Redis -> Socket Gateway -> Clients
+ \
+ -> materialization worker -> PostgreSQL durable totals
+```
+
+### 2.4 Why this flow is safer than direct synchronous cache updates
+
+- The user gets a correct committed score as soon as PostgreSQL commits.
+- Redis and sockets can lag briefly without losing the authoritative state.
+- The API process never has to solve “database commit succeeded but Kafka publish failed.”
+- Duplicate client retries are absorbed by the database uniqueness rule.
+- The write path avoids a hot per-user total row, which is better aligned with heavy append traffic.
+
+---
+
+## 3. Transaction + Outbox + CDC Flow
+
+### 3.1 Core idea
+
+The command API must never perform:
+
+- `INSERT score_events`
+- then separately `publish("score-updated")`
+
+as two unrelated operations.
+
+If the process crashes between them, the database and event stream diverge.
+
+### 3.2 ASCII flow
+
+```text
+┌──────────────────────────────────────────────────────────────────────┐
+│ score-command-api │
+│ │
+│ BEGIN │
+│ 1. INSERT score_action_requests │
+│ 2. INSERT score_events │
+│ 3. INSERT outbox_events │
+│ COMMIT │
+└───────────────────────────────┬──────────────────────────────────────┘
+ │ committed rows only
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ CDC Connector │
+│ - watches committed outbox rows │
+│ - transforms row into event payload │
+│ - publishes to Kafka topic keyed by user_id │
+└───────────────────────────────┬──────────────────────────────────────┘
+ ▼
+┌──────────────────────────────────────────────────────────────────────┐
+│ Kafka │
+│ - durable event log │
+│ - replay source for projection consumers │
+│ - isolates write path from read-side failure │
+└──────────────────────────────────────────────────────────────────────┘
+```
+
+### 3.3 Design rules
+
+- `outbox_events` must be written in the same PostgreSQL transaction as the accepted score event.
+- CDC must only read committed rows.
+- Kafka topic partition key should be `user_id` to preserve per-user ordering.
+- Consumers must assume at-least-once delivery and deduplicate using `event_id` or another monotonic applied-position marker.
+- Durable PostgreSQL totals, if present, must be maintained asynchronously and never move back into the write transaction.
+
+### 3.4 What the outbox payload should contain
+
+- event id
+- event type
+- user id
+- action id
+- score delta
+- occurred at
+- metadata for fraud and auditing
+
+---
+
+## 4. Projection, Leaderboard Query, and Live Update Flow
+
+### 4.1 Responsibilities of the projection service
+
+- consume `score-events`
+- update Redis leaderboard state
+- update per-user read models
+- compute or refresh top-10 snapshot
+- decide whether a socket broadcast is necessary
+- expose read APIs directly, or back a thin read-only API layer
+- optionally feed a durable PostgreSQL materialization worker
+
+### 4.2 Projection execution flow
+
+```text
+TIME │ COMPONENT │ ACTION
+─────┼────────────────────────────┼─────────────────────────────────────────────────────────────
+ 1 │ Kafka │ score.user_score_updated.v1 is available
+ │
+ 2 │ Projection Service │ Poll batch from assigned partition(s)
+ │ │ Buffer for 100-250ms or until N events
+ │
+ 3 │ Projection Service │ Coalesce multiple events for same user if possible
+ │ │ Keep highest unapplied event_id per user in the batch
+ │
+ 4 │ Redis Dedup Check │ Read current last_applied_event_id for user
+ │ │ Ignore stale or duplicate events
+ │
+ 5 │ Redis Leaderboard Update │ ZINCRBY leaderboard:global scoreDelta userId
+ │ │ HINCRBY or script-update user:score:{userId}
+ │ │ Refresh leaderboard:top10:snapshot if needed
+ │
+ 6 │ Fraud Window Update │ ZADD fraud:user:{userId}:events timestamp eventId
+ │ │ ZADD fraud:ip:{ip}:events timestamp eventId
+ │
+ 7 │ Broadcast Decision │ Use rank-10 boundary checks as a cheap prefilter
+ │ │ If visibility might be affected, recompute visible top 10
+ │ │ Compare with last published snapshot or snapshot hash
+ │ │ Emit one coalesced leaderboard:update only on real diff
+ │
+ 8 │ Durable Materialization │ Optionally upsert PostgreSQL materialized totals in batch
+ │ │ using last included event_id as checkpoint
+ │
+ 9 │ Offset Commit │ Commit Kafka offset only after required projection work succeeds
+```
+
+### 4.3 Projection Mermaid sequence
+
+```mermaid
+sequenceDiagram
+ participant K as Kafka
+ participant S as score-projection-service
+ participant R as Redis
+ participant W as Socket Gateway
+ participant C as Client
+
+ K->>S: Deliver batch of score events
+ S->>S: Coalesce by user_id / highest unapplied event_id
+ S->>R: Compare current last_applied_event_id
+ alt incoming event is stale
+ R-->>S: stored event id is newer or equal
+ S->>S: Skip event
+ else incoming event is newer
+ S->>R: ZINCRBY leaderboard:global scoreDelta
+ S->>R: Update user:score:{userId}
+ S->>R: Update fraud rolling windows
+ S->>R: Check user rank and current rank-10 boundary
+ alt cannot affect visible top 10
+ S->>S: Skip socket publish
+ else might affect visible top 10
+ S->>R: Read current top-10 snapshot
+ S->>S: Compare with last published snapshot/hash
+ alt snapshot differs
+ S->>W: leaderboard:update
+ W-->>C: Push realtime change
+ else snapshot unchanged
+ S->>S: Skip socket publish
+ end
+ end
+ end
+ S->>K: Commit offset after Redis success
+```
+
+### 4.3.1 ASCII alternative
+
+```text
+Kafka
+ |
+ v
+score-projection-service
+ |
+ +--> poll batch
+ +--> coalesce by user_id / highest event_id
+ +--> compare incoming event_id with stored last_applied_event_id
+ |
+ +--> stale or duplicate -> skip
+ |
+ +--> newer event:
+ |
+ +--> ZINCRBY Redis leaderboard:global
+ +--> update Redis user:score:{userId}
+ +--> update Redis fraud windows
+ +--> check user rank + current rank-10 boundary
+ |
+ +--> cannot affect visible top 10 -> skip socket publish
+ |
+ +--> might affect visible top 10:
+ |
+ +--> read current top-10 snapshot
+ +--> compare with last published snapshot/hash
+ |
+ +--> unchanged -> skip socket publish
+ |
+ +--> changed:
+ |
+ +--> emit leaderboard:update to Socket Gateway
+ |
+ +--> push update to clients
+ |
+ +--> commit Kafka offset only after Redis work succeeds
+```
+
+### 4.3.2 Recommended broadcast rule
+
+- Do not publish purely because one user score changed.
+- Use rank-10 boundary checks only as a cheap prefilter.
+- Final publish truth should be:
+ - recompute the visible top-10 payload
+ - compare it with the last published snapshot or a stored snapshot hash
+ - publish only if the visible payload changed
+
+Why this is the safer default:
+
+- It catches a user entering top 10.
+- It catches a user leaving top 10.
+- It catches a displaced rank-10 user.
+- It catches reordering among already-visible users.
+- It remains correct for micro-batches where multiple users move together.
+
+Recommended implementation shape:
+
+- Keep the last published top-10 payload or a deterministic hash of it.
+- After a batch update, only perform the snapshot diff if at least one affected user:
+ - was already in top 10
+ - moved near the rank-10 cutoff
+ - or could displace the current rank-10 entry
+- If the prefilter says the batch cannot affect visibility, skip the socket publish path entirely.
+
+### 4.4 Query path
+
+```text
+GET /v1/leaderboard/top10
+ |
+ v
+score-projection-service or thin query API
+ |
+ +--> try Redis live projection (leaderboard:top10 or leaderboard:global)
+ |
+ +--> if Redis available:
+ | |
+ | +--> return latest projected leaderboard
+ |
+ +--> if Redis unavailable:
+ |
+ +--> read PostgreSQL user_score_materialized_totals
+ +--> return stale/degraded response with freshness metadata
+```
+
+```text
+GET /v1/users/:userId/score
+ |
+ v
+score-projection-service or thin query API
+ |
+ +--> try Redis user projection (user:score:{userId})
+ |
+ +--> if Redis available:
+ | |
+ | +--> return latest projected user score + rank metadata
+ |
+ +--> if Redis unavailable:
+ |
+ +--> read PostgreSQL user_score_materialized_totals
+ +--> compute degraded rank if supported, or return score-only fallback
+```
+
+### 4.5 Why batching matters
+
+- It reduces Redis write volume.
+- It reduces socket fanout pressure.
+- It keeps the system stable when many users score at once.
+- The product still gets near-real-time updates because the buffer window is small.
+
+---
+
+## 5. Fraud Detection and Abuse Evaluation
+
+### 5.1 Fraud goals
+
+The system must make malicious score inflation harder without turning the write path into a slow manual-review workflow.
+
+### 5.2 Fraud decision flow
+
+```text
+ Is access token valid?
+ │
+ ┌─────────┴─────────┐
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ Is action allowed? Return 401
+ │
+ ┌────────┴────────┐
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ Is idempotency key new? Return 403 / 422
+ │
+ ┌──────┴──────┐
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ Evaluate fraud indicators Return original response
+ │
+ ┌────────┼─────────────────────────────────────────────────────┐
+ │ │ │ │ │
+ ▼ ▼ ▼ ▼ ▼
+ user burst duplicate device/IP impossible rate score jump rank jump
+ in window reuse across users for action too large too sudden
+ │ │ │ │ │
+ └────────┴──────────────┬──────┴────────────────┴──────────────┘
+ │
+ ▼
+ Severity threshold exceeded?
+ │
+ ┌────────┴────────┐
+ │ │
+ NO YES
+ │ │
+ ▼ ▼
+ continue normal write persist fraud_flags
+ and tag metadata optionally emit fraud event
+```
+
+### 5.3 Fraud signal sources
+
+- current request metadata
+- action frequency per user
+- action frequency by IP or device
+- known cooldown violations
+- impossible action sequences
+- extreme score velocity
+
+### 5.4 Fraud state ownership
+
+- Redis owns rolling windows and fast signal evaluation.
+- PostgreSQL owns durable `fraud_flags`.
+- Admin or moderation tools should read from durable PostgreSQL state, not Redis-only signals.
+
+### 5.5 Fraud Mermaid view
+
+```mermaid
+flowchart TD
+ Request[Incoming action completion request]
+ Auth[Auth + action validation]
+ Idempotency[Idempotency key check]
+ Signals[Velocity / duplicate / impossible pattern checks]
+ Persist[Persist fraud_flags if severe]
+ Accept[Continue normal score write]
+ Reject[Reject request]
+
+ Request --> Auth
+ Auth -->|invalid| Reject
+ Auth -->|valid| Idempotency
+ Idempotency -->|duplicate request| Accept
+ Idempotency -->|new request| Signals
+ Signals -->|severe| Persist
+ Signals -->|normal or mild| Accept
+ Persist --> Accept
+```
+
+### 5.5.1 ASCII alternative
+
+```text
+Incoming action completion request
+ |
+ v
+Auth + action validation
+ |
+ +--> invalid --------------------> Reject
+ |
+ v
+Idempotency key check
+ |
+ +--> duplicate request ----------> Return original accepted result
+ |
+ v
+Velocity / duplicate / impossible-pattern checks
+ |
+ +--> severe ---------------------> Persist fraud_flags
+ | |
+ | v
+ +--------------------------------> Continue normal score write
+```
+
+---
+
+## 6. Crash Recovery and Replay Flow
+
+### 6.1 Failure scenarios we care about
+
+1. API crashes before PostgreSQL commit.
+2. API crashes after PostgreSQL commit but before the client receives the response.
+3. CDC connector or Kafka is temporarily unavailable.
+4. Projection service crashes after Redis write but before Kafka offset commit.
+5. Redis is lost and must be rebuilt.
+6. Durable PostgreSQL materialized totals lag behind the live Redis projection.
+
+### 6.2 Recovery behavior by scenario
+
+| Failure point | Expected behavior | Why it is safe |
+| -------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------- |
+| before DB commit | client retries | no committed score exists yet |
+| after DB commit, before client sees response | client retries with same idempotency key | original response is returned from `score_action_requests` |
+| CDC lag or Kafka outage | write path still commits to PostgreSQL | outbox rows remain durable until streamed |
+| projection crash before offset commit | Kafka replays event | Redis last-applied-event check ignores stale duplicate |
+| Redis wipe | rebuild from Kafka retention or PostgreSQL ledger/materialized totals | Redis is derived state |
+| materialized totals lag | serve Redis as primary; use PG only as degraded fallback | materialized totals are intentionally asynchronous |
+
+### 6.3 Replay flow ASCII
+
+```text
+┌──────────────────────────────────────────────────────────────────────┐
+│ Event consumed from Kafka │
+└───────────────────────────────┬──────────────────────────────────────┘
+ ▼
+ ┌───────────────────────────────┐
+│ Apply Redis projection │
+│ event_id = 9455331 │
+ └───────────────┬───────────────┘
+ │
+ crash before offset commit?
+ │
+ ┌─────────┴─────────┐
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ process restarts commit offset normally
+ │
+ ▼
+ Kafka redelivers event
+ │
+ ▼
+ Redis already has last_applied_event_id >= incoming event_id
+ │
+ ▼
+ Ignore stale duplicate safely
+ │
+ ▼
+ Commit offset
+```
+
+### 6.4 Replay Mermaid sequence
+
+```mermaid
+sequenceDiagram
+ participant K as Kafka
+ participant S as score-projection-service
+ participant R as Redis
+
+ K->>S: Deliver event(event_id=9455331)
+ S->>R: Apply projection(event_id=9455331)
+ alt service crashes before offset commit
+ Note over S: process exits unexpectedly
+ K->>S: Replay same event after restart
+ S->>R: Read stored last_applied_event_id
+ R-->>S: stored event_id already 9455331
+ S->>S: Ignore stale duplicate
+ S->>K: Commit offset
+ else normal execution
+ S->>K: Commit offset
+ end
+```
+
+### 6.4.1 ASCII alternative
+
+```text
+Kafka delivers event(event_id=9455331)
+ |
+ v
+score-projection-service applies Redis projection
+ |
+ +--> crash before offset commit?
+ |
+ +--> no -> commit offset
+ |
+ +--> yes -> process restarts
+ |
+ v
+ Kafka redelivers same event
+ |
+ v
+ projection service reads Redis last_applied_event_id
+ |
+ +--> stored event_id already 9455331
+ |
+ v
+ ignore stale duplicate safely
+ |
+ v
+ commit offset
+```
+
+### 6.5 Redis rebuild strategy
+
+```text
+Option A: Kafka retained history available
+ Kafka replay -> projection service -> rebuild Redis
+
+Option B: durable PostgreSQL materialized totals available
+ load materialized totals -> replay later events only -> rebuild Redis
+
+Option C: Kafka history insufficient and no recent materialized seed
+ PostgreSQL score_events backfill job -> synthetic replay -> rebuild Redis
+```
+
+This rebuild path should be documented before production launch. Otherwise Redis stops being safely disposable in practice.
+
+---
+
+## 7. Authentication and Session Flow
+
+### 7.1 Why PostgreSQL-backed session state is included
+
+The user asked for a design that highlights database skill. Storing refresh/session state in PostgreSQL shows:
+
+- revocation capability
+- device and IP correlation
+- auditability for security incidents
+
+### 7.2 Auth flow
+
+```text
+┌──────────────────────────────────────────────────────────────────────┐
+│ 1. User authenticates │
+└───────────────────────────────┬──────────────────────────────────────┘
+ ▼
+ ┌────────────────────────────────┐
+ │ Server issues short-lived JWT │
+ │ and stores refresh/session row │
+ │ in PostgreSQL user_sessions │
+ └───────────────┬────────────────┘
+ │
+ ┌────────────────┴────────────────┐
+ │ │
+ ▼ ▼
+ ┌─────────────────────┐ ┌─────────────────────────┐
+ │ HTTP score mutation │ │ WebSocket connection │
+ │ Authorization: Bearer JWT │ token in header/query │
+ └────────────┬────────┘ └────────────┬────────────┘
+ │ │
+ ▼ ▼
+ ┌─────────────────────┐ ┌─────────────────────────┐
+ │ Verify JWT │ │ Verify JWT │
+ │ Check revocation if │ │ Reject unauthorized │
+ │ policy requires it │ │ connections │
+ └─────────────────────┘ └─────────────────────────┘
+```
+
+### 7.3 Session-related checks
+
+- rejected if token invalid or expired
+- optionally rejected if backing session is revoked
+- action request stores session, device, and IP metadata for later analysis
+
+---
+
+## 8. Error Handling and Consistency Boundaries
+
+### 8.1 Error handling map
+
+```text
+ Incoming request
+ │
+ ┌───────────────────────┼────────────────────────┐
+ │ │ │
+ ▼ ▼ ▼
+ schema invalid token invalid forbidden action
+ │ │ │
+ ▼ ▼ ▼
+ return 400 return 401 return 403 / 422
+ │
+ ▼
+ database transaction starts
+ │
+ duplicate idempotency key detected?
+ │
+ ┌───────────┴───────────┐
+ │ │
+ YES NO
+ │ │
+ ▼ ▼
+ return stored original response continue write
+```
+
+### 8.2 Consistency notes
+
+- The success response from `POST /v1/actions/complete` is authoritative.
+- The success response confirms event acceptance, not immediate query-side total score.
+- `GET /v1/leaderboard/top10` is eventually consistent.
+- WebSocket updates are projection-driven, not transaction-driven.
+- The product should expect small propagation delay under normal operation.
+- Durable PostgreSQL materialized totals can lag Redis and should be treated as fallback, not primary live ranking state.
+
+### 8.3 Client-facing implications
+
+- If a client cares about “my new score right now,” it should use the write response.
+- If a client needs immediate UI feedback, it can optimistically add `scoreDelta` locally until the projection catches up.
+- If a client cares about “global top 10 right now,” it should use the projected read API or realtime update stream.
+
+---
+
+## 9. Deployment and Scaling Architecture
+
+### 9.1 Deployment topology
+
+```text
+ ┌──────────────────┐
+ │ Internet Users │
+ └────────┬─────────┘
+ ▼
+ ┌────────────────────────────┐
+ │ API Gateway / Load Balancer│
+ └──────────────┬─────────────┘
+ │
+ ┌─────────────────────────────┼─────────────────────────────┬──────────────────────┐
+ │ │ │ │
+ ▼ ▼ ▼ ▼
+┌──────────────────┐ ┌────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
+│ command-api pod 1│ │ query/projection │ │ socket gateway 1 │ │ socket gateway N │
+│ command-api pod 2│ │ pod 1 │ │ socket gateway 2 │ └──────────────────┘
+│ command-api pod N│ │ pod N │ └─────────▲────────┘
+└─────────┬────────┘ └─────────┬──────────┘ │
+ │ │ │ leaderboard fanout
+ ▼ │ │
+ ┌──────────────┐ │ │
+ │ PostgreSQL │ │ │
+ │ primary │ │ │
+ │ + replicas │ │ │
+ └──────┬───────┘ │ │
+ │ │ │
+ │ committed outbox rows │ │
+ ▼ │ │
+ ┌──────────────┐ │ │
+ │ CDC Connector│ │ │
+ │ Debezium │ │ │
+ └──────┬───────┘ │ │
+ ▼ │ │
+ ┌──────────────┐ │ │
+ │ Kafka │──────────────────┘ │
+ │ cluster │───────────────────────────────────────────────┘
+ └──────┬───────┘
+ │
+ ├──> consumed by query/projection pods (scale by Kafka partitions)
+ │
+ └──> consumed by materialization worker
+ |
+ v
+ ┌──────────────┐
+ │ PostgreSQL │
+ │ materialized │
+ │ totals │
+ └──────┬───────┘
+ │
+ v
+ ┌──────────────┐
+ │ backups / DR │
+ └──────────────┘
+
+query/projection pods
+ |
+ +--> serve GET read APIs
+ +--> update Redis live query model
+ +--> publish leaderboard fanout to socket gateways
+
+Redis live query model
+ |
+ +--> leaderboard keys
+ +--> user score projections
+ +--> fraud windows
+```
+
+### 9.2 Primary scaling dimensions
+
+- `score-command-api`
+ - scale by incoming write request volume
+- projection consumers
+ - scale by Kafka partition count and event throughput
+- materialization workers
+ - scale by replay and durable-fallback throughput needs
+- socket gateways
+ - scale by concurrent live connections
+- PostgreSQL
+ - scale carefully; keep transactions append-heavy on the write path and batch materialized updates
+- Redis
+ - scale by read QPS, top-N traffic, and socket fanout support
+
+### 9.3 Operational note
+
+The write path and read path can scale independently. That is one of the main reasons to keep them separated.
+
+---
+
+## 10. Suggested Monitoring and Alerts
+
+### 10.1 Key metrics
+
+- API latency for `POST /v1/actions/complete`
+- database transaction latency
+- idempotency replay rate
+- outbox backlog growth
+- CDC lag
+- Kafka consumer lag
+- Redis update latency
+- leaderboard projection freshness
+- socket broadcast latency
+- fraud flag rate
+
+### 10.2 Alert examples
+
+| Metric | Threshold example | Response |
+| ----------------------- | ----------------------------- | ----------------------------------------- |
+| command API p99 latency | > 500ms sustained | inspect DB contention, slow queries |
+| outbox backlog | growing for 5+ minutes | inspect CDC connector |
+| Kafka consumer lag | above agreed SLO | scale projection workers or inspect Redis |
+| Redis latency | > 50-100ms sustained | inspect hot keys / cluster pressure |
+| projection freshness | older than live-update target | degrade banner or alert on-call |
+| fraud rate spike | above baseline | inspect abuse campaign or client bug |
+
+### 10.3 Runbook expectations
+
+Before production, the team should have runbooks for:
+
+- Kafka lag spike
+- Redis rebuild
+- projection consumer poison event
+- revoked-session incident
+- admin score correction or reversal
+
+---
+
+## 11. Final Implementation Guidance
+
+If the backend team follows this document:
+
+- PostgreSQL remains the single source of truth.
+- the write path stays correct under retries and concurrency.
+- downstream failure does not lose accepted scores.
+- Redis stays fast but disposable.
+- leaderboard updates remain near-real-time without coupling correctness to socket fanout.
+
+That is the core difference between an interview-level CRUD answer and a production-oriented scoreboard design.
diff --git a/src/problem6/IMPROVEMENTS.md b/src/problem6/IMPROVEMENTS.md
new file mode 100644
index 0000000000..c20ed485e4
--- /dev/null
+++ b/src/problem6/IMPROVEMENTS.md
@@ -0,0 +1,628 @@
+# Problem 6 Improvements
+
+This document describes how to evolve the base architecture in `README.md` and `ARCHITECTURE.md` after the first correct implementation is in place.
+
+The main principle is:
+
+- do not compromise correctness on the write path
+- add scale and operational sophistication in stages
+- introduce complexity only when the product actually needs it
+
+---
+
+## 1. Improvement Strategy Overview
+
+The proposed architecture is already strong, but it should not be shipped as "everything on day one" unless the product really needs it.
+
+### Improvement roadmap
+
+```text
+Stage 1: Correctness Core
+ PostgreSQL + idempotent API + append-only ledger + outbox + Redis live projection + simple live fanout
+
+Stage 2: Replayable Streaming Read Side
+ CDC + Kafka + projection workers + Redis event-id dedupe + durable PG materialization + replay tooling
+
+Stage 3: Operational Maturity
+ multi-node socket fanout + richer fraud scoring + admin workflows + DR / archival
+```
+
+### Decision rule
+
+```text
+If product traffic is modest and the team is small:
+ start with Stage 1
+
+If multiple read-side consumers or rebuildability matter:
+ add Stage 2
+
+If uptime, compliance, moderation, and global scale matter:
+ add Stage 3
+```
+
+---
+
+## 2. Stage 1 Improvements: Correctness First
+
+Stage 1 is the minimum strong production shape for a real scoreboard system.
+
+### What Stage 1 should include
+
+- PostgreSQL as source of truth
+- `POST /v1/actions/complete` with idempotency keys
+- immutable `score_events` ledger
+- transactional outbox table
+- Redis live leaderboard projection and per-user score cache
+- single-region socket fanout
+
+### Why this stage matters
+
+- prevents score duplication on retries
+- keeps the write path append-only and light
+- creates a durable audit trail
+- leaves room to add streaming later without redesigning the core write path
+
+### Example: retry-safe request behavior
+
+Client sends:
+
+```json
+{
+ "actionId": "daily-login",
+ "idempotencyKey": "0f83c365-f1c5-4f1c-a17d-4227e9433f89"
+}
+```
+
+If the client times out and retries with the same key:
+
+- the API must not add score twice
+- the API should return the original accepted response body
+
+### Stage 1 flow
+
+```text
+Client
+ |
+ v
+score-command-api
+ |
+ +--> validate token
+ +--> validate action
+ +--> INSERT request(idempotency key)
+ +--> INSERT score_events
+ +--> INSERT outbox_events
+ +--> COMMIT
+ |
+ +--> projection catches up asynchronously
+ |
+ +--> emit single-region live update
+```
+
+### Suggested Stage 1 additions
+
+- add deterministic tie-breakers early
+- store the accepted response body on the request table
+- record `deviceId`, `sessionId`, and `ip` with every accepted score change
+
+---
+
+## 3. Stage 2 Improvements: Replayable Read Side
+
+Stage 2 is where the system becomes much easier to recover and evolve.
+
+### What Stage 2 should include
+
+- CDC from PostgreSQL outbox into Kafka
+- dedicated projection consumers
+- Redis dedupe checks using `last_applied_event_id`
+- durable PostgreSQL materialized totals for replay seeding and degraded fallback
+- replay/backfill tooling
+- dead-letter handling for poison events
+
+### Why this stage matters
+
+- separates the write path from read-side failures
+- allows Redis rebuild without re-accepting writes
+- supports more than one downstream consumer
+- supports richer analytics and fraud consumers later
+
+### Example: projection idempotency
+
+Kafka delivers the same event twice:
+
+```json
+{
+ "eventId": 9455331,
+ "userId": "user-123",
+ "scoreDelta": 50
+}
+```
+
+If Redis already stores `lastAppliedEventId = 9455331` for that user, the consumer should:
+
+- skip the duplicate
+- not re-emit a visible leaderboard update
+- commit the Kafka offset after the check
+
+### Stage 2 flow
+
+```text
+PostgreSQL commit
+ |
+ v
+outbox row committed
+ |
+ v
+CDC connector
+ |
+ v
+Kafka topic score-events
+ |
+ v
+projection service
+ |
+ +--> compare eventId with Redis last_applied_event_id
+ +--> ZINCRBY leaderboard keys
+ +--> update user score document
+ +--> optionally upsert PostgreSQL materialized totals
+ +--> emit socket update if visible state changed
+ +--> commit offset
+```
+
+### Recommended Stage 2 operational additions
+
+- dead-letter topic for malformed or poison events
+- replay command that rebuilds one scope at a time
+- freshness metric for projected leaderboard lag
+
+### Example replay command shape
+
+```text
+rebuild leaderboard scope:
+ source = Kafka retained topic
+ target = Redis keys for global leaderboard only
+ mode = stop writes? no
+ result = rebuild derived state without touching PostgreSQL truth
+```
+
+---
+
+## 4. Stage 3 Improvements: Operational Maturity
+
+Stage 3 is appropriate when the scoreboard becomes a business-critical or abuse-sensitive system.
+
+### What Stage 3 should include
+
+- multi-node socket fanout with Redis or broker-backed adapter
+- richer fraud scoring model
+- admin correction and reversal workflows
+- long-retention archival for rebuilds
+- disaster recovery and region failover planning
+
+### Why this stage matters
+
+- scaling live connections is a different problem from scaling writes
+- moderation and support teams eventually need safe correction tools
+- retention and recovery matter more once the leaderboard becomes user-visible business state
+
+### Stage 3 maturity ladder
+
+```text
+Single node live updates
+ ->
+multiple socket nodes with shared fanout bus
+ ->
+admin score review and reversal tooling
+ ->
+long-retention replay support
+ ->
+regional failover / DR plan
+```
+
+---
+
+## 5. Product and Ranking Improvements
+
+The initial problem only asks for top 10 live scores. Real products usually need more.
+
+### 5.1 Deterministic tie-breakers
+
+Do not leave equal scores unordered.
+
+Recommended order:
+
+1. `total_score DESC`
+2. `reached_score_at ASC`
+3. `action_count DESC`
+4. `user_id ASC`
+
+### Example
+
+```text
+User A score = 5000, reached at 10:00, action_count = 70
+User B score = 5000, reached at 10:05, action_count = 80
+
+Result:
+ User A ranks higher because the same score was reached earlier.
+```
+
+### 5.2 Scoped leaderboards
+
+Recommended future scopes:
+
+- daily leaderboard
+- weekly leaderboard
+- monthly leaderboard
+- friend leaderboard
+- region leaderboard
+
+### Example API shapes
+
+```text
+GET /v1/leaderboard/top10?period=day
+GET /v1/leaderboard/top10?period=week
+GET /v1/leaderboard/top10?scope=friends
+GET /v1/leaderboard/rank-neighbors?userId=user-123&window=5
+```
+
+### Scoped leaderboard model
+
+```text
+global leaderboard
+ |
+ +--> all-time
+ +--> daily
+ +--> weekly
+ +--> monthly
+ +--> friends
+ +--> region
+```
+
+### 5.3 Rank-neighbor queries
+
+Top 10 is not enough for most users.
+
+Example:
+
+```text
+User rank = 248
+Need to show:
+ ranks 243-253
+instead of only the global top 10
+```
+
+This reduces the product problem where most users never see themselves.
+
+---
+
+## 6. Admin Correction and Reversal Workflow
+
+Eventually the team needs a safe way to correct bad score state.
+
+### Recommended model
+
+- never silently overwrite score history
+- create immutable reversal or adjustment events
+- restrict adjustment endpoints to admin roles
+- always log the reason and operator identity
+
+### Example use cases
+
+- fraud investigation confirms a boosted account
+- a production bug double-awarded points
+- a moderator restores score after incorrect abuse enforcement
+
+### Recommended workflow
+
+```text
+Admin identifies incorrect score event
+ |
+ v
+Create adjustment / reversal request
+ |
+ v
+Admin API validates permissions
+ |
+ v
+PostgreSQL transaction
+ |
+ +--> append adjustment event
+ +--> write outbox event
+ +--> write audit trail
+ |
+ v
+CDC -> Kafka -> projection update -> clients see corrected leaderboard
+ |
+ +--> async PostgreSQL materialized totals catch up too
+```
+
+### Example adjustment payload
+
+```json
+{
+ "userId": "user-123",
+ "adjustmentType": "reversal",
+ "scoreDelta": -50,
+ "reason": "duplicate daily-login event from incident INC-1042"
+}
+```
+
+---
+
+## 7. Operational Improvements
+
+### 7.1 Monitoring additions
+
+Track at minimum:
+
+- command API latency
+- PostgreSQL transaction latency
+- idempotency replay rate
+- outbox backlog
+- CDC lag
+- Kafka consumer lag
+- Redis update latency
+- socket fanout latency
+- fraud flag rate
+
+### 7.2 Alert examples
+
+| Problem | Example symptom | Possible action |
+| --- | --- | --- |
+| Outbox stuck | `outbox_events` pending rows keep growing | inspect CDC connector and connector offsets |
+| Projection lag | top-10 snapshot is 2 minutes behind | scale consumers or inspect Redis slowdown |
+| Redis pressure | p95 Redis write latency spikes | reduce batch size churn, inspect hot keys |
+| Socket overload | client reconnects spike | scale socket gateways, inspect pub/sub bus |
+
+### 7.3 Replay/backfill tooling
+
+The team should be able to rebuild:
+
+- entire global leaderboard
+- one scoped leaderboard only
+- one user projection only
+
+### Replay tooling flow
+
+```text
+Need rebuild?
+ |
+ +--> one user only
+ | |
+ | +--> read score_events for that user
+ | +--> rebuild Redis user document
+ |
+ +--> one leaderboard scope
+ | |
+ | +--> replay relevant Kafka events or PostgreSQL backfill job
+ | +--> refresh snapshot keys only for that scope
+ |
+ +--> full rebuild
+ |
+ +--> stream historical events
+ +--> rebuild all derived Redis state
+```
+
+### 7.4 Backpressure controls
+
+If Redis becomes slow:
+
+- reduce socket emission frequency
+- enlarge micro-batch window temporarily
+- prioritize leaderboard snapshots over lower-priority auxiliary keys
+- if PostgreSQL materialized totals are updated in the same worker pool, batch them separately from latency-sensitive Redis work
+- pause or isolate non-critical derived consumers
+
+---
+
+## 8. Security Improvements
+
+### 8.1 Signed action grants
+
+If the client is not fully trusted, the server can issue a short-lived action grant before completion.
+
+Flow:
+
+```text
+Server issues action grant
+ |
+ v
+Client completes work and submits grant
+ |
+ v
+Command API verifies:
+ - signature
+ - expiry
+ - userId match
+ - actionId match
+```
+
+This makes arbitrary client-crafted score requests harder.
+
+### 8.2 Session and device correlation
+
+Keep the following with accepted actions:
+
+- `sessionId`
+- `deviceId`
+- `ip`
+- `userAgent`
+
+### Example fraud use
+
+```text
+Five accounts
+ |
+ +--> same IP
+ +--> same device fingerprint
+ +--> same high-value action every 3 seconds
+
+Result:
+ cluster should be flagged for analyst review
+```
+
+### 8.3 Permission separation
+
+Separate:
+
+- player permissions
+- moderator review permissions
+- admin score-adjust permissions
+
+Do not reuse the normal player write path for administrative changes.
+
+---
+
+## 9. Data Lifecycle Improvements
+
+### 9.1 Table partitioning
+
+Good partition candidates:
+
+- `score_events`
+- `fraud_flags`
+- possibly `outbox_events` if volume is very high
+
+### Example lifecycle
+
+```text
+Current month partitions
+ |
+ +--> score_events_2026_04
+ +--> score_events_2026_05
+ +--> score_events_2026_06
+
+Older partitions
+ |
+ +--> compressed / archived
+ +--> still queryable for audits if required
+```
+
+### 9.2 Retention policy
+
+- keep short-term request dedupe rows long enough for retry windows
+- keep ledger data much longer than cache data
+- do not treat Redis as long-term storage
+
+### 9.3 Compliance-sensitive storage
+
+Only store what Redis needs for realtime decisions.
+
+Prefer PostgreSQL for:
+
+- durable review data
+- operator actions
+- security-sensitive metadata
+
+---
+
+## 10. Realtime Delivery Improvements
+
+### 10.1 Multi-node socket fanout
+
+Single-node sockets work early, but not forever.
+
+Recommended evolution:
+
+```text
+Clients
+ |
+ v
+Socket Gateway 1 ----\
+Socket Gateway 2 -----+--> shared fanout bus (Redis adapter / broker)
+Socket Gateway N ----/
+```
+
+This ensures a projection update can reach clients connected to any gateway instance.
+
+### 10.2 Coalesced update strategy
+
+Instead of one socket message per raw score event:
+
+- buffer for 100-250ms
+- combine changes by user or by top-10 diff
+- emit one coalesced update
+
+### Example
+
+```text
+Without coalescing:
+ 300 score events in 1 second
+ -> 300 Redis writes
+ -> 300 socket pushes
+
+With coalescing:
+ 300 score events in 1 second
+ -> 20-40 projection batches
+ -> 20-40 socket pushes
+```
+
+### 10.3 SSE vs WebSocket
+
+If the product only needs one-way live scoreboard updates, SSE may be simpler.
+
+Use WebSockets when:
+
+- bidirectional client/server realtime traffic is needed
+- the team already has socket infrastructure
+- reconnect, rooms, or richer event semantics are already expected
+
+Use SSE when:
+
+- only server-to-client streaming is needed
+- simplicity is more valuable than socket flexibility
+
+---
+
+## 11. Anti-Fraud Evolution Path
+
+Fraud systems usually evolve in stages too.
+
+### Maturity ladder
+
+```text
+Stage A: rule-based checks
+ - rate limits
+ - duplicate detection
+ - impossible cooldown checks
+
+Stage B: clustered signals
+ - IP clusters
+ - device reuse
+ - score velocity groups
+
+Stage C: analyst-assisted models
+ - review queue
+ - operator tooling
+ - model feedback from confirmed incidents
+```
+
+### Practical recommendation
+
+Start with clear, explainable rules. Only add more advanced scoring once you have:
+
+- enough volume
+- enough labeled abuse cases
+- operator processes to review and act on flags
+
+---
+
+## 12. Recommended Priority Order
+
+If the team can only do a few things next, prioritize in this order:
+
+1. Store and replay original idempotent responses.
+2. Add deterministic tie-breakers.
+3. Add replay/backfill tooling for derived Redis state.
+4. Add admin adjustment and reversal workflow.
+5. Add multi-node socket fanout and richer fraud tooling only when the product needs them.
+
+### Final recommendation
+
+The best improvement philosophy for this system is:
+
+- correctness first
+- replayability second
+- operational maturity third
+
+That keeps the architecture strong without turning it into unnecessary platform complexity too early.
diff --git a/src/problem6/README.md b/src/problem6/README.md
new file mode 100644
index 0000000000..b4f11cada9
--- /dev/null
+++ b/src/problem6/README.md
@@ -0,0 +1,444 @@
+# Problem 6: Real-Time Scoreboard System
+
+This package implements the requested Problem 6 deliverable as an implementation-ready backend specification for a secure, real-time scoreboard.
+
+The design is intentionally centered on PostgreSQL correctness first, with Kafka and Redis used as downstream infrastructure for replayable read models and low-latency leaderboard delivery.
+
+This version uses an append-only write model:
+
+- the authoritative write path stores accepted score events
+- Redis maintains the live query model
+- PostgreSQL can keep an asynchronous durable materialized total for warm starts and degraded fallback
+- the write path does not synchronously update a current-total aggregate row
+
+Related documents:
+
+- `ARCHITECTURE.md` for flow diagrams
+- `IMPROVEMENTS.md` for rollout and follow-up ideas
+- `REVIEW.md` for plan review, best-practice assessment, and company-pattern research
+- `schema.sql` for a concrete PostgreSQL schema draft
+
+## Goals
+
+- Keep score writes correct under retries and concurrency.
+- Keep the write path append-only and lightweight under heavy score traffic.
+- Deliver near-real-time top-10 leaderboard updates.
+- Prevent unauthorized or replayed score inflation.
+- Make the read model disposable and rebuildable.
+- Show clear database engineering depth: locking, idempotency, auditability, and recovery.
+
+## Non-Goals
+
+- Defining the product-specific action catalog or game rules in detail.
+- Requiring exactly-once delivery end to end. The design targets at-least-once delivery with idempotent consumers.
+- Making Redis or WebSocket state authoritative.
+
+## System Topology
+
+### Command side
+
+- `score-command-api`
+ - Authenticates the caller.
+ - Validates the requested action.
+ - Appends the authoritative accepted score event in PostgreSQL.
+ - Writes an outbox event in the same transaction.
+
+### Data platform
+
+- `postgres`
+ - Source of truth for users, sessions, action requests, score ledger, fraud flags, outbox rows, and optional durable materialized totals.
+- `cdc-connector`
+ - Debezium or equivalent connector that reads committed outbox rows.
+- `kafka`
+ - Durable event transport and replay source for projection services.
+
+### Read side
+
+- `score-projection-service`
+ - Consumes score events from Kafka.
+ - Updates Redis leaderboard and score projections.
+ - Serves read APIs directly or powers a thin query API layer.
+ - Triggers live leaderboard fanout only when the visible top-10 payload actually changes.
+- `score-materialization-worker`
+ - Periodically or continuously persists durable PostgreSQL materialized totals from the event stream for warm starts, bounded replay, or degraded read fallback.
+- `redis`
+ - Read-optimized cache and ranking store.
+- `socket gateway`
+ - Fanout layer for clients subscribed to leaderboard updates.
+
+## Write API Contract
+
+### `POST /v1/actions/complete`
+
+Records a completed action and increases the user score if the request is valid and not already processed.
+
+#### Request
+
+```json
+{
+ "actionId": "daily-login",
+ "idempotencyKey": "45e31d35-4db2-40ae-9a7b-b63f6b151a11",
+ "actionInstanceId": "quest-run-2026-04-17-001",
+ "deviceId": "ios-14f02be",
+ "clientTimestamp": "2026-04-17T11:12:03.122Z"
+}
+```
+
+#### Authentication
+
+- Access token must identify the authenticated user.
+- Refresh token or session state should be revocable server-side through `user_sessions` in PostgreSQL.
+- The API must reject requests where the caller is not allowed to claim the requested action.
+
+#### Command-side steps
+
+1. Authenticate the caller and resolve `userId`.
+2. Validate request shape and action eligibility.
+3. Begin a PostgreSQL transaction.
+4. Insert into `score_action_requests` using `(user_id, idempotency_key)` uniqueness.
+5. If the key already exists:
+ - return the previously saved response body
+ - do not mutate score again
+6. Insert an immutable ledger row into `score_events` and return its `event_id`.
+7. Insert one or more `outbox_events` rows in the same transaction.
+8. Insert `fraud_flags` if the request trips suspicious heuristics.
+9. Persist the accepted API response back onto the request row.
+10. Commit and return acceptance of the event.
+
+Important consequence:
+
+- the command API acknowledges the accepted score event
+- it does not depend on Redis being updated first
+- it does not synchronously compute the final query-side total score
+
+#### Success response
+
+```json
+{
+ "requestId": "8d2d0f2c-6026-4c3e-9da1-91d42221f31c",
+ "acceptedEventId": 9455331,
+ "userId": "user-123",
+ "actionId": "daily-login",
+ "scoreDelta": 50,
+ "acceptedAt": "2026-04-17T11:12:03.240Z",
+ "projectionStatus": "pending",
+ "consistency": "event_accepted"
+}
+```
+
+This response confirms that the score event is durably accepted. The projected total score might appear slightly later in Redis and WebSocket updates.
+
+#### Error contract
+
+- `400 Bad Request`
+ - malformed body
+ - invalid idempotency key
+ - unsupported action
+- `401 Unauthorized`
+ - missing or invalid access token
+- `403 Forbidden`
+ - user is not allowed to claim the action
+- `409 Conflict`
+ - conflicting idempotency key reuse with different parameters
+- `422 Unprocessable Entity`
+ - action already consumed or business rule violated
+- `429 Too Many Requests`
+ - abuse or rate limit enforcement
+
+## Query API Contract
+
+### `GET /v1/leaderboard/top10`
+
+Returns the latest projected leaderboard snapshot from Redis.
+
+```json
+{
+ "lastAppliedEventId": 99122,
+ "generatedAt": "2026-04-17T11:12:03.390Z",
+ "isProjectionStale": false,
+ "entries": [
+ {
+ "rank": 1,
+ "userId": "user-742",
+ "displayName": "Sam",
+ "score": 21250
+ }
+ ]
+}
+```
+
+### `GET /v1/users/:userId/score`
+
+Returns the user-facing projected score state.
+
+```json
+{
+ "userId": "user-123",
+ "score": 1500,
+ "rank": 18,
+ "lastAppliedEventId": 9455331,
+ "updatedAt": "2026-04-17T11:12:03.390Z"
+}
+```
+
+## Realtime Contract
+
+### `wss://api.scoreboard.service/v1/leaderboard/live`
+
+Clients subscribe to push updates after projection, not directly from the command transaction.
+
+#### Initial payload
+
+```json
+{
+ "event": "leaderboard:initial",
+ "data": {
+ "lastAppliedEventId": 99122,
+ "generatedAt": "2026-04-17T11:12:03.390Z",
+ "entries": []
+ }
+}
+```
+
+#### Incremental update payload
+
+```json
+{
+ "event": "leaderboard:update",
+ "data": {
+ "leaderboardEventId": 99123,
+ "lastAppliedEventId": 9455331,
+ "generatedAt": "2026-04-17T11:12:03.440Z",
+ "entries": [
+ {
+ "rank": 1,
+ "userId": "user-742",
+ "displayName": "Sam",
+ "score": 21250
+ }
+ ]
+ }
+}
+```
+
+Recommended delivery semantics:
+
+- `leaderboard:update` should represent the latest visible top-10 snapshot after projection.
+- The projection service may use rank-10 boundary checks as a cheap prefilter, but the publish decision should come from diffing the visible snapshot against the last published snapshot or snapshot hash.
+
+## Consistency Model
+
+- PostgreSQL is authoritative for accepted writes.
+- Redis is eventually consistent and may lag behind accepted writes.
+- Clients should treat the `POST /v1/actions/complete` response as authoritative confirmation that the event was accepted.
+- Clients should treat total score, rank, and top-10 membership as projection-based state.
+- Leaderboard queries and socket updates are projection-based and may trail by milliseconds to seconds under load.
+- If the product needs an immediate UI update, it should use optimistic rendering from `scoreDelta` until the projection catches up.
+
+## PostgreSQL Data Model
+
+Core tables:
+
+- `users`
+- `actions`
+- `user_sessions`
+- `score_action_requests`
+- `score_events`
+- `outbox_events`
+- `fraud_flags`
+- `user_score_materialized_totals`
+- `projection_checkpoints`
+
+Important database decisions:
+
+- Idempotency is enforced with a unique constraint, not only application logic.
+- If `actionInstanceId` is supplied, a per-user/per-action uniqueness rule can also prevent double-claiming the same action occurrence with a different idempotency key.
+- `score_events` is immutable and append-only.
+- `score_events.score_delta` is constrained to positive values in the current additive-only design.
+- `outbox_events` is written in the same transaction as the authoritative mutation.
+- `user_score_materialized_totals` is asynchronous and should not be part of the write transaction.
+- Durable fallback totals are a materialized view, not the authoritative write model.
+- Fraud review state is durable in PostgreSQL even if Redis windows are lost.
+
+Important design distinction:
+
+- `score_events` is the source of truth
+- Redis is the live query model
+- `user_score_materialized_totals` is a durable asynchronous materialized view for warm starts, bounded replay, or degraded fallback
+- if aggregate replay ever becomes expensive, snapshot events or snapshot rows can be added later as an optimization
+
+## Why No Synchronous Total Row
+
+For a write-heavy scoreboard, continuously updating a per-user total row can create unnecessary write amplification and contention on a hot aggregate.
+
+This design instead prefers:
+
+- append-only `score_events` on the write path
+- Redis materialized views for live totals and rankings
+- asynchronous PostgreSQL materialized totals for replay seeding or degraded fallback
+
+This is a better fit when:
+
+- score updates are additive
+- the product accepts eventual consistency for rank and total reads
+- the API can acknowledge event acceptance without promising that the projection is already caught up
+
+See `schema.sql` for a concrete table/index draft.
+
+## Kafka Event Model
+
+Topic:
+
+- `score-events`
+
+Partition key:
+
+- `user_id`
+
+Event example:
+
+```json
+{
+ "eventId": 9455331,
+ "eventType": "score.user_score_updated.v1",
+ "userId": "user-123",
+ "actionId": "daily-login",
+ "scoreDelta": 50,
+ "occurredAt": "2026-04-17T11:12:03.240Z",
+ "request": {
+ "idempotencyKey": "45e31d35-4db2-40ae-9a7b-b63f6b151a11",
+ "sessionId": "c9ecdd67-c52a-4d30-bb41-0bf613fd27da",
+ "deviceId": "ios-14f02be",
+ "ip": "203.0.113.10"
+ }
+}
+```
+
+Event rules:
+
+- Each consumer must treat the stream as at-least-once.
+- Consumers must ignore stale or duplicate event IDs that were already applied for the same user.
+- Schema evolution should use explicit versioned event types.
+
+## Redis Read Model
+
+Recommended keys:
+
+- `leaderboard:global`
+ - sorted set keyed by `user_id`
+ - updated with `ZINCRBY` using `score_delta`
+- `leaderboard:top10:snapshot`
+ - optional cached JSON snapshot for fast reads
+- `user:score:{userId}`
+ - hash or JSON blob with score, rank, `last_applied_event_id`, and update time
+- `fraud:user:{userId}:events`
+ - sorted set scored by event timestamp
+- `fraud:ip:{ip}:events`
+ - sorted set for burst detection by IP
+
+Recommendation:
+
+- do not rely on TTL expiry for the core leaderboard sorted set or per-user live projection
+- let the projection service keep these keys current
+- if you use TTL at all, use it only on secondary convenience snapshots such as `leaderboard:top10:snapshot`
+
+Key query patterns:
+
+- `ZREVRANGE leaderboard:global 0 9 WITHSCORES`
+- `ZREVRANK leaderboard:global {userId}`
+
+## Projection Service
+
+The projection service owns Redis updates and real-time fanout.
+
+Responsibilities:
+
+- Consume `score-events` from Kafka.
+- Apply only unseen `event_id` values for each user.
+- Increment the global leaderboard sorted set.
+- Maintain per-user score documents with the last applied event ID.
+- Use rank-10 boundary checks as a prefilter for realtime fanout work.
+- Recompute and diff the visible top-10 snapshot before publishing `leaderboard:update`.
+- Maintain short-lived Redis fraud windows for anomaly detection.
+- Optionally feed or coordinate the durable PostgreSQL materialization worker.
+
+Recommended broadcast rule:
+
+- do not publish purely because one user score changed
+- after applying a batch, first check whether any affected user could impact the visible top 10
+- only then read the current top-10 snapshot and compare it with the last published snapshot or snapshot hash
+- publish realtime updates only when the visible payload actually changed
+
+Recommended batching:
+
+- Buffer events per partition for `100-250ms`, or
+- flush after `N` events
+
+This avoids one Redis write plus one socket message per raw score event during spikes.
+
+## Durable PostgreSQL Materialization
+
+For heavy-write systems, a durable PostgreSQL fallback can exist without returning to a synchronous aggregate row on the write path.
+
+Recommended model:
+
+- `user_score_materialized_totals` is updated asynchronously by a separate materialization worker in the default architecture.
+- Smaller deployments may choose to fold this into the projection service later, but that is an implementation simplification, not the current recommended topology.
+- each row stores the latest total score, action count, and `last_event_id` included in that materialization
+
+Use cases:
+
+- warm-start Redis after a restart
+- bound replay cost by starting from the last materialized event
+- serve degraded reads if Redis is unavailable
+
+This is not a TTL-driven cache refresh mechanism. It is a durable asynchronous materialized view.
+
+## Fraud and Abuse Controls
+
+Fraud protection should be layered:
+
+- authenticated users only
+- action allow-list with server-owned score values
+- idempotency key requirement for every write
+- session/device/IP metadata capture
+- Redis sliding windows for burst and velocity detection
+- durable PostgreSQL fraud flags for analyst review
+- admin tools for manual freeze, reversal, or account review
+
+Redis should provide fast signals, not final truth. Durable review actions belong in PostgreSQL.
+
+## Recovery Model
+
+- Kafka offsets are committed only after Redis projection succeeds.
+- Redis stores the latest applied `event_id` per user.
+- Replayed events with older or equal IDs are ignored for that user.
+- Projection handlers should persist a checkpoint or last-seen event ID so restart/resume is exact.
+- If Redis is lost, rebuild it from Kafka retention or from PostgreSQL `score_events`.
+- If full replay is expensive, seed Redis from `user_score_materialized_totals` and replay only later events.
+- If Kafka retention is not long enough for rebuild, back up the ledger to long-retention storage or use PostgreSQL backfill jobs.
+
+## Operational Expectations
+
+- Metrics
+ - request success rate
+ - idempotency replay rate
+ - transaction latency
+ - Kafka consumer lag
+ - projection freshness
+ - socket fanout latency
+ - fraud flag rate
+- Alerts
+ - projection lag threshold exceeded
+ - outbox growth without downstream delivery
+ - Redis rebuild active
+ - dead-letter queue or poison event growth
+
+## Why This Is Strong
+
+- The write path is correct even when downstream systems fail.
+- The write path is append-only and lighter under heavy score traffic.
+- The read side is fast and disposable.
+- Recovery is explicit instead of implied.
+- Idempotency remains first-class, while the system avoids a hot per-user total row on the write path.
+- The design can start small and grow into a larger streaming platform without changing the core correctness model.
diff --git a/src/problem6/schema.sql b/src/problem6/schema.sql
new file mode 100644
index 0000000000..8f81e3b44e
--- /dev/null
+++ b/src/problem6/schema.sql
@@ -0,0 +1,133 @@
+BEGIN;
+
+CREATE TABLE users (
+ id BIGSERIAL PRIMARY KEY,
+ external_id VARCHAR(128) NOT NULL UNIQUE,
+ display_name VARCHAR(120) NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE actions (
+ id BIGSERIAL PRIMARY KEY,
+ code VARCHAR(64) NOT NULL UNIQUE,
+ score_delta INTEGER NOT NULL CHECK (score_delta > 0),
+ cooldown_seconds INTEGER NOT NULL DEFAULT 0 CHECK (cooldown_seconds >= 0),
+ is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE user_sessions (
+ session_id UUID PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id),
+ refresh_token_hash TEXT NOT NULL,
+ device_id VARCHAR(128),
+ ip INET,
+ user_agent TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL,
+ revoked_at TIMESTAMPTZ,
+ last_seen_at TIMESTAMPTZ
+);
+
+CREATE TABLE score_action_requests (
+ request_id UUID PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id),
+ action_id BIGINT NOT NULL REFERENCES actions(id),
+ session_id UUID REFERENCES user_sessions(session_id),
+ idempotency_key VARCHAR(255) NOT NULL,
+ action_instance_id VARCHAR(128),
+ request_payload JSONB NOT NULL,
+ response_code INTEGER,
+ response_body JSONB,
+ requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ completed_at TIMESTAMPTZ,
+ UNIQUE (user_id, idempotency_key)
+);
+
+CREATE TABLE score_events (
+ event_id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id),
+ action_id BIGINT NOT NULL REFERENCES actions(id),
+ request_id UUID NOT NULL REFERENCES score_action_requests(request_id),
+ score_delta INTEGER NOT NULL CHECK (score_delta > 0),
+ session_id UUID REFERENCES user_sessions(session_id),
+ device_id VARCHAR(128),
+ ip INET,
+ occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE (request_id)
+);
+
+CREATE TABLE outbox_events (
+ outbox_id BIGSERIAL PRIMARY KEY,
+ aggregate_type VARCHAR(64) NOT NULL,
+ aggregate_id VARCHAR(128) NOT NULL,
+ event_type VARCHAR(128) NOT NULL,
+ partition_key VARCHAR(128) NOT NULL,
+ payload JSONB NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ published_at TIMESTAMPTZ
+);
+
+CREATE TABLE fraud_flags (
+ flag_id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id),
+ score_event_id BIGINT REFERENCES score_events(event_id),
+ signal_type VARCHAR(64) NOT NULL,
+ severity SMALLINT NOT NULL CHECK (severity BETWEEN 1 AND 5),
+ details JSONB NOT NULL,
+ status VARCHAR(32) NOT NULL DEFAULT 'open',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ reviewed_at TIMESTAMPTZ
+);
+
+CREATE TABLE user_score_materialized_totals (
+ user_id BIGINT PRIMARY KEY REFERENCES users(id),
+ total_score BIGINT NOT NULL DEFAULT 0,
+ action_count BIGINT NOT NULL DEFAULT 0,
+ reached_score_at TIMESTAMPTZ,
+ last_event_id BIGINT NOT NULL REFERENCES score_events(event_id),
+ materialized_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE TABLE projection_checkpoints (
+ projection_name VARCHAR(100) PRIMARY KEY,
+ last_event_id BIGINT NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX idx_user_sessions_user_id_active
+ ON user_sessions (user_id, revoked_at, expires_at);
+
+CREATE INDEX idx_score_action_requests_requested_at
+ ON score_action_requests (requested_at DESC);
+
+CREATE UNIQUE INDEX idx_score_action_requests_user_action_instance
+ ON score_action_requests (user_id, action_id, action_instance_id)
+ WHERE action_instance_id IS NOT NULL;
+
+CREATE INDEX idx_score_events_user_occurred_at_desc
+ ON score_events (user_id, occurred_at DESC);
+
+CREATE INDEX idx_score_events_action_occurred_at_desc
+ ON score_events (action_id, occurred_at DESC);
+
+CREATE INDEX idx_score_events_occurred_at_desc
+ ON score_events (occurred_at DESC);
+
+CREATE INDEX idx_outbox_events_created_at
+ ON outbox_events (created_at);
+
+CREATE INDEX idx_outbox_events_unpublished
+ ON outbox_events (published_at, created_at)
+ WHERE published_at IS NULL;
+
+CREATE INDEX idx_fraud_flags_user_created_at_desc
+ ON fraud_flags (user_id, created_at DESC);
+
+CREATE INDEX idx_user_score_materialized_totals_last_event_id
+ ON user_score_materialized_totals (last_event_id);
+
+COMMIT;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..bde33a358f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "types": ["node"],
+ "esModuleInterop": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "rootDir": "."
+ },
+ "include": ["src/problem4/**/*.ts", "src/problem5/**/*.ts"]
+}