From bc451c30825bc67801bb02bfdf5c1cb606e19cc2 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 12 Feb 2026 16:10:56 -0700 Subject: [PATCH 01/79] Main starter files --- shatter-backend/package-lock.json | 846 ++++++++++++++++++++++- shatter-backend/package.json | 4 +- shatter-backend/src/ai/gemini.ts | 119 ++++ shatter-backend/src/ai/prompts/bingo.txt | 0 4 files changed, 954 insertions(+), 15 deletions(-) create mode 100644 shatter-backend/src/ai/gemini.ts create mode 100644 shatter-backend/src/ai/prompts/bingo.txt diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index dd99515..44db55b 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/genai": "^1.41.0", "axios": "^1.13.5", "bcryptjs": "^3.0.3", "cors": "^2.8.5", @@ -18,7 +19,8 @@ "mongoose": "^8.19.2", "pusher": "^5.3.2", "socket.io": "^4.8.1", - "zod": "^4.1.12" + "zod": "^4.3.6", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@eslint/js": "^9.38.0", @@ -244,6 +246,50 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/genai": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.41.0.tgz", + "integrity": "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^7.1.1", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/genai/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -296,6 +342,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@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/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -371,6 +434,80 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -937,6 +1074,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -954,11 +1100,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/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/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1019,7 +1176,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/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/base64id": { @@ -1040,6 +1216,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1213,7 +1398,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1226,7 +1410,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -1311,7 +1494,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1322,6 +1504,15 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1410,6 +1601,12 @@ "xtend": "^4.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1425,6 +1622,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1814,6 +2017,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1862,6 +2071,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1963,6 +2195,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/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/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -2000,6 +2248,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2049,6 +2309,99 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/gaxios/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "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/gaxios/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2134,6 +2487,68 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2153,6 +2568,40 @@ "dev": true, "license": "MIT" }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2227,6 +2676,19 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2352,6 +2814,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2365,6 +2836,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2385,9 +2868,23 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/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/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2411,6 +2908,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2573,6 +3079,18 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2684,6 +3202,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -2818,6 +3345,26 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -2962,6 +3509,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz", + "integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==", + "license": "MIT", + "dependencies": { + "is-network-error": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3008,7 +3576,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3021,6 +3588,22 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/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": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -3054,6 +3637,30 @@ "node": ">= 0.8.0" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3369,7 +3976,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3382,7 +3988,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3466,6 +4071,18 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/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/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -3640,6 +4257,102 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/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/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/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/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -3953,6 +4666,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -3979,7 +4701,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4001,6 +4722,94 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/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/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/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/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/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/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4062,13 +4871,22 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/shatter-backend/package.json b/shatter-backend/package.json index a9c6f6f..d289a90 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -13,6 +13,7 @@ "license": "ISC", "description": "", "dependencies": { + "@google/genai": "^1.41.0", "axios": "^1.13.5", "bcryptjs": "^3.0.3", "cors": "^2.8.5", @@ -22,7 +23,8 @@ "mongoose": "^8.19.2", "pusher": "^5.3.2", "socket.io": "^4.8.1", - "zod": "^4.1.12" + "zod": "^4.3.6", + "zod-to-json-schema": "^3.25.1" }, "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/shatter-backend/src/ai/gemini.ts b/shatter-backend/src/ai/gemini.ts new file mode 100644 index 0000000..827fb56 --- /dev/null +++ b/shatter-backend/src/ai/gemini.ts @@ -0,0 +1,119 @@ +import { GoogleGenAI } from "@google/genai"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import "dotenv/config"; + + +const bingoSchema = z.object({ + grid: z.array(z.string()), +}); + +export type Bingo = z.infer; + +const ai = new GoogleGenAI({ + apiKey: process.env.GOOGLE_API_KEY, +}); + +// ---------------------- +// Helpers +// ---------------------- + +function makeEmptyGrid(size: number): string[] { + return Array.from({ length: size }, () => ""); +} + +function extractJsonObject(text: string): string { + const trimmed = text.trim(); + if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed; + + const first = trimmed.indexOf("{"); + const last = trimmed.lastIndexOf("}"); + if (first !== -1 && last !== -1 && last > first) { + return trimmed.slice(first, last + 1); + } + + return trimmed; +} + + + +export async function generateNameBingo( + n_rows: number, + n_cols: number +): Promise { + const totalCells = n_rows * n_cols; + + const prompt = ` +Generate a Name Bingo game. + +Requirements: +- Return ONLY valid JSON (no markdown, no commentary). +- The ONLY top-level field must be "grid". +- "grid" must be a 1D array of exactly ${totalCells} strings. +- Each element must be a RANDOM full name (first + last). +- All names should be unique. +- If you cannot generate valid JSON, return {"grid": ${JSON.stringify( + makeEmptyGrid(totalCells) + )}}. + +Schema: +${JSON.stringify(zodToJsonSchema(bingoSchema), null, 2)} +`.trim(); + + let rawResponse: string | undefined; + + try { + const response = await ai.models.generateContent({ + model: "gemini-3-flash-preview", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: zodToJsonSchema(bingoSchema), + temperature: 0.7, + }, + }); + + rawResponse = + typeof response.text === "string" + ? response.text + : String(response.text); + + const jsonText = extractJsonObject(rawResponse); + const parsed = JSON.parse(jsonText); + const validated = bingoSchema.parse(parsed); + + if (validated.grid.length !== totalCells) { + return { grid: makeEmptyGrid(totalCells) }; + } + + return validated; + } catch (error) { + console.error("\n❌ Generation Failed\n"); + + if (rawResponse) { + console.error("---- Raw Gemini Response ----"); + console.error(rawResponse); + console.error("-----------------------------\n"); + } else { + console.error("No response text was received from Gemini.\n"); + } + + console.error("Error:", error); + + return { grid: makeEmptyGrid(totalCells) }; + } +} + +// ---------------------- +// Run directly +// ---------------------- + +async function main() { + const result = await generateNameBingo(3, 3); + console.log("\n✅ Generated Bingo Grid:\n"); + console.log(JSON.stringify(result, null, 2)); +} + +main().catch(() => { + process.exitCode = 1; +}); diff --git a/shatter-backend/src/ai/prompts/bingo.txt b/shatter-backend/src/ai/prompts/bingo.txt new file mode 100644 index 0000000..e69de29 From bcaffd429bbbbd7acedb717f83f5976d99b26236 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 12 Feb 2026 17:24:14 -0700 Subject: [PATCH 02/79] Utility Prompt Builder Class --- shatter-backend/src/ai/gemini.ts | 65 ++++++++++++--------- shatter-backend/src/ai/prompt_builder.ts | 74 ++++++++++++++++++++++++ shatter-backend/src/ai/prompts/bingo.txt | 9 +++ 3 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 shatter-backend/src/ai/prompt_builder.ts diff --git a/shatter-backend/src/ai/gemini.ts b/shatter-backend/src/ai/gemini.ts index 827fb56..4fb39f0 100644 --- a/shatter-backend/src/ai/gemini.ts +++ b/shatter-backend/src/ai/gemini.ts @@ -1,15 +1,23 @@ +// src/ai/gemini.ts import { GoogleGenAI } from "@google/genai"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; - +// ---------------------- +// Schema (1D array of names) +// ---------------------- const bingoSchema = z.object({ grid: z.array(z.string()), }); export type Bingo = z.infer; +// ---------------------- +// AI Setup +// ---------------------- const ai = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY, }); @@ -17,7 +25,6 @@ const ai = new GoogleGenAI({ // ---------------------- // Helpers // ---------------------- - function makeEmptyGrid(size: number): string[] { return Array.from({ length: size }, () => ""); } @@ -35,30 +42,35 @@ function extractJsonObject(text: string): string { return trimmed; } +function readPromptTemplate(): string { + // Resolves relative to this file’s location reliably + const filePath = path.resolve(process.cwd(), "src", "ai", "prompts", "bingo.txt"); + return fs.readFileSync(filePath, "utf8"); +} +function buildPromptFromTemplate(template: string, totalCells: number): string { + const schemaJson = JSON.stringify(zodToJsonSchema(bingoSchema), null, 2); + const emptyGridJson = JSON.stringify(makeEmptyGrid(totalCells)); + + // Supported placeholders in bingo.txt: + // {{TOTAL_CELLS}} -> total number of names to generate + // {{SCHEMA}} -> JSON schema + // {{EMPTY_GRID}} -> JSON array fallback for grid + return template + .replace(/{{TOTAL_CELLS}}/g, String(totalCells)) + .replace(/{{SCHEMA}}/g, schemaJson) + .replace(/{{EMPTY_GRID}}/g, emptyGridJson) + .trim(); +} -export async function generateNameBingo( - n_rows: number, - n_cols: number -): Promise { +// ---------------------- +// Generator +// ---------------------- +export async function generateNameBingo(n_rows: number, n_cols: number): Promise { const totalCells = n_rows * n_cols; - const prompt = ` -Generate a Name Bingo game. - -Requirements: -- Return ONLY valid JSON (no markdown, no commentary). -- The ONLY top-level field must be "grid". -- "grid" must be a 1D array of exactly ${totalCells} strings. -- Each element must be a RANDOM full name (first + last). -- All names should be unique. -- If you cannot generate valid JSON, return {"grid": ${JSON.stringify( - makeEmptyGrid(totalCells) - )}}. - -Schema: -${JSON.stringify(zodToJsonSchema(bingoSchema), null, 2)} -`.trim(); + const template = readPromptTemplate(); + const prompt = buildPromptFromTemplate(template, totalCells); let rawResponse: string | undefined; @@ -73,15 +85,13 @@ ${JSON.stringify(zodToJsonSchema(bingoSchema), null, 2)} }, }); - rawResponse = - typeof response.text === "string" - ? response.text - : String(response.text); + rawResponse = typeof response.text === "string" ? response.text : String(response.text); const jsonText = extractJsonObject(rawResponse); const parsed = JSON.parse(jsonText); const validated = bingoSchema.parse(parsed); + // Enforce exact length; fallback if wrong if (validated.grid.length !== totalCells) { return { grid: makeEmptyGrid(totalCells) }; } @@ -107,9 +117,8 @@ ${JSON.stringify(zodToJsonSchema(bingoSchema), null, 2)} // ---------------------- // Run directly // ---------------------- - async function main() { - const result = await generateNameBingo(3, 3); + const result = await generateNameBingo(4, 4); console.log("\n✅ Generated Bingo Grid:\n"); console.log(JSON.stringify(result, null, 2)); } diff --git a/shatter-backend/src/ai/prompt_builder.ts b/shatter-backend/src/ai/prompt_builder.ts new file mode 100644 index 0000000..578e8e6 --- /dev/null +++ b/shatter-backend/src/ai/prompt_builder.ts @@ -0,0 +1,74 @@ +/** + * Prompt builder utility class. + * + * Allows incremental construction of a prompt string + * from multiple parts, joined by a configurable separator. + */ +class Prompt { + + private promptParts: string[]; + private separator: string; + private promptString: string; + + + /** + * Creates a new Prompt instance. + * + * @param arr - Initial array of prompt parts. + * @param separator - String used to separate parts when generating the full prompt. + */ + public constructor(arr: string[] = [], separator: string = "\n\n") { + this.promptParts = arr; + this.separator = separator; + this.promptString = arr.join(this.separator); + } + + /** + * Returns the current array of prompt parts. + * + * @returns Array of prompt segments. + */ + public getPromptParts(): string[] { + return this.promptParts; + } + + /** + * Replaces the current prompt parts with a new array + * and regenerates the full prompt string. + * + * @param arr - New array of prompt segments. + */ + public setPromptParts(arr: string[]): void { + this.promptParts = arr; + this.generatePrompt(); + } + + /** + * Adds a new part to the prompt and regenerates + * the full prompt string. + * + * @param part - A string segment to append to the prompt. + */ + public addPart(part: string): void { + this.promptParts.push(part); + this.generatePrompt(); + } + + /** + * Regenerates the full prompt string by joining + * all prompt parts using the configured separator. + */ + public generatePrompt(): void { + this.promptString = this.promptParts.join(this.separator); + } + + /** + * Returns the fully generated prompt string. + * + * @returns The complete prompt as a single string. + */ + public getPrompt(): string { + return this.promptString; + } + +} diff --git a/shatter-backend/src/ai/prompts/bingo.txt b/shatter-backend/src/ai/prompts/bingo.txt index e69de29..d5a9c2f 100644 --- a/shatter-backend/src/ai/prompts/bingo.txt +++ b/shatter-backend/src/ai/prompts/bingo.txt @@ -0,0 +1,9 @@ +Generate a Name Bingo game. + +Requirements: +- Return ONLY valid JSON (no markdown, no commentary). +- The ONLY top-level field must be "grid". +- "grid" must be a 1D array of exactly {{TOTAL_CELLS}} strings. +- Each element must be a RANDOM full name (first + last). +- All names should be unique. +- If you cannot generate valid JSON, return {"grid": {{EMPTY_GRID}} }. From c730a856de4e386ae1f07e9c3e7fb3f602541add Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 8 Mar 2026 17:25:34 -0600 Subject: [PATCH 03/79] Add duplicate name suffix, nested participant populate, and auth error improvements - Allow duplicate participant names by appending random #XXX suffix on collision instead of blocking with 409 error (both guest and authenticated join flows) - Return populated participantIds (name, userId) in GET /users/:userId/events to enable frontend participant connection loading - Return LinkedIn-specific error message when a LinkedIn user tries local signup - Update all relevant docs (API reference, schema, realtime guide, roadmap) --- shatter-backend/docs/API_REFERENCE.md | 21 +- shatter-backend/docs/DATABASE_SCHEMA.md | 3 +- shatter-backend/docs/REALTIME_EVENTS_GUIDE.md | 4 +- shatter-backend/docs/name-bingo-roadmap.md | 1082 +++++++++++++++++ .../src/controllers/auth_controller.ts | 7 +- .../src/controllers/event_controller.ts | 75 +- .../src/controllers/user_controller.ts | 10 +- 7 files changed, 1166 insertions(+), 36 deletions(-) create mode 100644 shatter-backend/docs/name-bingo-roadmap.md diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index f1f50a8..097cdf7 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -1,6 +1,6 @@ # Shatter Backend — API Reference -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 **Base URL:** `http://localhost:4000/api` --- @@ -169,7 +169,8 @@ Create a new user account. | 400 | `"name, email and password are required"` | | 400 | `"Invalid email format"` | | 400 | `"Password must be at least 8 characters long"` | -| 409 | `"Email already exists"` | +| 409 | `"Email already exists"` (local account) | +| 409 | `"This email is associated with a LinkedIn account. Please log in with LinkedIn."` (LinkedIn account) | --- @@ -420,12 +421,18 @@ Get all events a user has joined (populates event details). "joinCode": "12345678", "startDate": "2025-02-01T18:00:00.000Z", "endDate": "2025-02-01T21:00:00.000Z", - "currentState": "In Progress" + "currentState": "In Progress", + "participantIds": [ + { "_id": "666b...", "name": "John Doe", "userId": "664f..." }, + { "_id": "666c...", "name": "Jane Smith", "userId": "664e..." } + ] } ] } ``` +**Note:** Each event's `participantIds` is populated with participant `name` and `userId` fields, enabling the frontend to load participant connections. + **Error Responses:** | Status | Error | @@ -716,13 +723,13 @@ Join an event as a registered (authenticated) user. | 404 | `"User not found"` | | 404 | `"Event not found"` | | 409 | `"User already joined"` | -| 409 | `"This name is already taken in this event"` | **Special Behavior:** - Creates a Participant record linking user to event +- If the display name is already taken in the event, a `#XXX` suffix is automatically appended (e.g., `John` becomes `John#472`). The response `participant.name` reflects the final display name. - Adds participant to event's `participantIds` array - Adds event to user's `eventHistoryIds` array -- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` +- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` (using the final display name) --- @@ -767,13 +774,13 @@ Join an event as a guest (no account required). | 400 | `"Missing fields: guest name and eventId are required"` | | 400 | `"Event is full"` | | 404 | `"Event not found"` | -| 409 | `"This name is already taken in this event"` | **Special Behavior:** - Creates a guest User (`authProvider: 'guest'`, no email/password) +- If the display name is already taken in the event, a `#XXX` suffix is automatically appended (e.g., `John` becomes `John#472`). The response `participant.name` reflects the final display name, and the guest User's name is updated to match. - Returns a JWT so the guest can make authenticated requests - Guest can later upgrade to a full account via `PUT /api/users/:userId` -- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` +- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` (using the final display name) --- diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index 97b840b..a7ba2cd 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -1,6 +1,6 @@ # Shatter Backend — Database Schema Reference -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 **Database:** MongoDB with Mongoose ODM **Collections:** 6 @@ -157,6 +157,7 @@ ### Key Behaviors - The compound unique index on `(eventId, name)` is case-insensitive, so "John" and "john" are treated as the same name within an event. +- When a name collision occurs during join, the backend automatically appends a random `#XXX` suffix (e.g., `John#472`) and retries, allowing multiple participants with the same base name. - No timestamps are enabled on this model. --- diff --git a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md index 987aeea..07641e4 100644 --- a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md +++ b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md @@ -1,6 +1,6 @@ # Shatter Backend — Real-Time Events Guide -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 --- @@ -106,7 +106,7 @@ Each event has its own channel. Subscribe when a user enters an event, unsubscri | Field | Type | Description | |-----------------|----------|-------------| | `participantId` | ObjectId | The new participant's ID | -| `name` | string | The participant's display name | +| `name` | string | The participant's display name (may include a `#XXX` suffix if the name was already taken in the event, e.g., `"John#472"`) | **Use case:** Update the live participant list in the event lobby/dashboard without polling. diff --git a/shatter-backend/docs/name-bingo-roadmap.md b/shatter-backend/docs/name-bingo-roadmap.md new file mode 100644 index 0000000..ede1783 --- /dev/null +++ b/shatter-backend/docs/name-bingo-roadmap.md @@ -0,0 +1,1082 @@ +# Name Bingo Backend Roadmap + +This roadmap outlines backend development tasks for the Name Bingo feature. Tasks are organized by priority and dependency order. + +**Scope**: Backend API development only. Frontend (mobile/web) tasks are out of scope but noted as dependencies. + +--- + +## Verification Report + +This roadmap has been cross-referenced against: +- `feature_list.md` (MVP requirements) +- `bingo_walkthrough.md` (user flow requirements) +- Mobile app implementation (`mobile-guest-refactor` branch) + +### Coverage Summary + +| Category | Status | +|----------|--------| +| MVP Backend APIs | ~95% (minor gaps addressed below) | +| Bingo Walkthrough | ~90% (Section 10 gaps addressed below) | +| Data Models | Documented explicitly | +| Lifecycle | Fully covered | +| Real-time | Fully covered | +| Auth | Partially covered (LinkedIn OAuth complete, LinkedIn account linking for guests missing) | +| Participant Connections | Fully implemented (model, CRUD, routes — access control for guests missing) | +| Guest Account Upgrade | Partially covered (email/password upgrade works, LinkedIn linking missing) | +| Mobile Bingo Gameplay | Client-side implementation complete (see Mobile Bingo Implementation Status) | + +### Gaps Addressed in This Version + +| Gap | Resolution | Location | +|-----|------------|----------| +| GET /api/users/:userId (Get Profile) | Added | Phase 3.1 | +| GET /api/users/:userId/current-event | Added | Phase 3.5 | +| profilePhoto in participant search | Added | Phase 6.5 | +| Rate limiting middleware | Added | Phase 6.10 | +| allowFreeTextEntry config | Deferred to Phase 6 | Phase 6.6 | +| gridSize in Bingo model | Deferred to Phase 6 | Phase 6.6 | +| Input sanitization | Added to Phase 4.1 | +| Guest account upgrade via LinkedIn linking | Added | Phase 0.3 Task B | +| Connections access control for guest users | Added | Phase 0.3 Task C | +| Organizer Analytics endpoints (feature_list.md "Wants") | Added as future item | Phase 6.11 | +| `gameType` and `eventImg` fields on Event model | Added | Phase 1.1 | +| `currentState` enum matching mobile `EventState` | Added | Phase 1.2 | +| Event status transition API (priority raised) | Moved from Phase 2 to Phase 1 | Phase 1.3 | + +--- + +## Current Implementation Status + +| Component | Status | Location | +|-----------|--------|----------| +| Authentication (signup/login) | Complete | `auth_controller.ts`, `auth_routes.ts` | +| JWT Middleware | Complete | `auth_middleware.ts`, `jwt_utils.ts` | +| User Model & CRUD | Complete | `user_model.ts`, `user_controller.ts` | +| Event Create/Join/Get | Complete | `event_controller.ts`, `event_model.ts` | +| Participant Model | Complete | `participant_model.ts` | +| Guest Join Flow (with User creation, `#XXX` name suffix on collision) | Complete | `event_controller.ts` | +| Profile Update Endpoint | Complete | `user_controller.ts`, `user_route.ts` | +| Pusher Real-time Setup | Complete | `pusher_websocket.ts` | +| Bingo CRUD (basic) | Complete | `bingo_controller.ts`, `bingo_model.ts` | +| LinkedIn OAuth | Complete | `linkedin_oauth.ts`, `auth_controller.ts` | +| Auth Code Exchange | Complete | `auth_code_model.ts`, `auth_controller.ts` | +| Quick Signup (Guest Join) | Complete | `event_controller.ts`, `user_model.ts` | +| ParticipantConnection Model | Complete | `participant_connection_model.ts` | +| ParticipantConnection CRUD | Complete | `participant_connections_controller.ts`, `participant_connections_routes.ts` | +| Guest Upgrade (Email/Password) | Complete | `user_controller.ts` (`updateUser` — sets password, upgrades authProvider) | +| Documentation (API, Real-time, Schema, Lifecycle) | Complete | `docs/API_REFERENCE.md`, `REALTIME_EVENTS_GUIDE.md`, `DATABASE_SCHEMA.md`, `EVENT_LIFECYCLE.md` | +| QR Code Generation | Complete (web client-side) | `shatter-web/src/components/QRCard.tsx` (uses `qrcode.react`, no backend needed) | +| Guest Upgrade (LinkedIn Linking) | Planned | Phase 0.3 Task B | +| Connections Access Control (Guests) | Planned | Phase 0.3 Task C | +| Event Model (`gameType`, `eventImg`) | Complete | Phase 1.1 | +| Event Status Enum (`currentState`) | Complete | Phase 1.2 | +| Event Status Transition API | Complete | Phase 1.3 | +| Player Game State | Deferred (mobile handles client-side) | Phase 6 | +| Participant Search | Deferred (mobile uses existing participant list) | Phase 6 | +| Event Lifecycle Transitions | Complete | Phase 1.3 | + +--- + +## Mobile Bingo Implementation Status + +The `mobile-guest-refactor` branch implements significant client-side bingo gameplay, which reduces the backend work needed for MVP. The following features are handled entirely on the mobile client: + +| Feature | Mobile Implementation | Backend Dependency | +|---------|----------------------|-------------------| +| Bingo grid display | Renders grid from `GET /api/bingo/getBingo/:eventId` categories | Existing endpoint (works) | +| Name assignment to cards | Autocomplete modal using participant list from event data | Existing `participantIds` on event (works) | +| Duplicate name prevention | Client-side validation — can't assign same name to two cards | None | +| Win detection (rows, cols, diagonals) | Client-side logic checks all win conditions | None | +| Blackout detection & animation | Client-side check for all cells filled + animation | None | +| Game state persistence | AsyncStorage — survives app restart | None | +| Lobby → game transition | Polls event status, transitions when `currentState` changes | **Needs Phase 1.2 + 1.3** | +| Event image display | Renders `eventImg` from event data | **Needs Phase 1.1** | +| Game type display | Renders `gameType` from event data | **Needs Phase 1.1** | + +### Mobile Enum Values (source of truth: `shatter-mobile/src/interfaces/Event.tsx`) + +```typescript +export enum EventState { + UPCOMING = "Upcoming", + IN_PROGRESS = "In Progress", + COMPLETED = "Completed", + INVALID = "Invalid", +} + +export enum GameType { + NAME_BINGO = "Name Bingo" +} +``` + +**Backend must use these exact string values** for `currentState` and `gameType` enums to maintain compatibility. + +--- + +## Phase 0: Authentication Enhancements (Critical Priority) + +These features are specified in `bingo_walkthrough.md` Section 3.1 as primary authentication methods. + +### 0.1 LinkedIn OAuth Integration ✅ COMPLETE + +**Endpoints**: +- `GET /api/auth/linkedin` - Initiates OAuth flow (redirects to LinkedIn) +- `GET /api/auth/linkedin/callback` - OAuth callback (creates/updates user, redirects with auth code) +- `POST /api/auth/exchange` - Exchange single-use auth code for JWT token + +**Implementation** (custom, no Passport dependency): +- `src/utils/linkedin_oauth.ts` - LinkedIn API helpers (`getLinkedInAuthUrl`, `getLinkedInAccessToken`, `getLinkedInProfile`) +- `src/models/auth_code_model.ts` - Single-use auth code model (60s TTL, auto-expires) +- `src/controllers/auth_controller.ts` - `linkedinAuth`, `linkedinCallback`, `exchangeAuthCode` +- `src/routes/auth_routes.ts` - All routes registered + +**User Model Fields** (in `user_model.ts`): +- `linkedinId` (String, unique, sparse) - LinkedIn subject ID +- `linkedinUrl` (String, unique, sparse) - LinkedIn profile URL +- `authProvider` (Enum: 'local' | 'linkedin', default 'local') +- `profilePhoto` (String) - populated from LinkedIn picture + +**Security**: +- CSRF protection via JWT-encoded state token (5-minute expiry) +- Auth code is single-use (atomic `findOneAndDelete`) with 60-second TTL +- JWT token never exposed in redirect URLs +- Email conflict detection (prevents duplicate accounts) + +**Env Vars**: `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, `LINKEDIN_CALLBACK_URL`, `FRONTEND_URL` + +**Frontend Integration**: +1. Open browser to `GET /api/auth/linkedin` +2. User authenticates with LinkedIn +3. Backend redirects to `{FRONTEND_URL}/auth/callback?code=` +4. Frontend calls `POST /api/auth/exchange` with `{ "code": "" }` +5. Response: `{ "message": "Authentication successful", "userId": "...", "token": "..." }` + +--- + +### 0.2 Quick Signup via Guest Join ✅ COMPLETE + +**Approach**: Instead of a separate signup endpoint, guest users are created automatically when joining an event. + +**Endpoint**: `POST /api/events/:eventId/join/guest` +- Takes only `{ name }` in request body +- Creates a User record with `authProvider: 'guest'` (no email/password required) +- Returns `{ success, participant, userId, token }` — guest gets a JWT immediately + +**Profile Completion**: `PUT /api/users/:userId` (protected, self-only) +- Guests can add email, password, bio, profilePhoto, socialLinks later +- Setting a password upgrades `authProvider` from `'guest'` to `'local'` +- Email validated for format and uniqueness, password must be >= 8 chars + +**Model Changes** (`user_model.ts`): +- `email` is now optional with `sparse: true` (allows multiple guest users without email) +- `authProvider` enum: `'local' | 'linkedin' | 'guest'` +- Added `bio` (String) and `socialLinks` (linkedin, github, other) fields + +**Implementation**: +- `src/controllers/event_controller.ts` — upgraded `joinEventAsGuest` +- `src/controllers/user_controller.ts` — added `updateUser` +- `src/routes/user_route.ts` — added `PUT /:userId` route + +**Frontend Dependency**: Mobile app guest join flow + profile completion screen. + +--- + +### 0.3 Guest Account Upgrade Flow (High Priority) + +**Reference**: `bingo_walkthrough.md` Section 10 — Guest Account Completion Flow + +Guest users can join events and play games without interruption, but they cannot access saved connections until they upgrade their account. The backend must support two upgrade paths and enforce connections access control. + +#### Task A: Email/Password Upgrade ✅ COMPLETE + +**Endpoint**: `PUT /api/users/:userId` (existing, protected, self-only) + +Already implemented in `user_controller.ts` (`updateUser`): +- Guest sets `email` + `password` via profile update +- Setting a password automatically upgrades `authProvider` from `'guest'` to `'local'` +- Email validated for format and uniqueness, password must be >= 8 chars + +No additional backend work required. + +#### Task B: LinkedIn Account Linking for Guest Users + +**Endpoint**: `POST /api/auth/linkedin/link` (new, protected) + +**Purpose**: Allow an authenticated guest user to attach their LinkedIn account, upgrading their `authProvider` from `'guest'` to `'linkedin'`. + +**Current gap**: The existing `linkedinCallback` in `auth_controller.ts` rejects requests when an email already exists (duplicate account prevention). It has no logic to link LinkedIn credentials to an existing guest account. + +**Implementation**: +- New endpoint that accepts an authenticated guest user's JWT +- Initiates or completes LinkedIn OAuth and attaches `linkedinId`, `linkedinUrl`, and `profilePhoto` to the existing user +- Updates `authProvider` from `'guest'` to `'linkedin'` +- Alternative approach: Modify the existing `linkedinCallback` to detect when the authenticated user is a guest and link instead of rejecting + +**Request flow**: +1. Authenticated guest user calls `POST /api/auth/linkedin/link` (or is redirected through a linking-specific OAuth flow) +2. Backend exchanges LinkedIn auth code for profile data +3. Backend attaches LinkedIn fields to existing guest user document +4. Returns updated user profile + existing JWT remains valid + +**Security**: +- Only users with `authProvider: 'guest'` can use this endpoint +- LinkedIn account must not already be linked to another user +- Validate JWT and confirm `req.user.userId` matches the account being linked + +**Files to modify/create**: +- `src/controllers/auth_controller.ts` — add `linkLinkedIn` handler (or modify `linkedinCallback`) +- `src/routes/auth_routes.ts` — register new route +- `src/utils/linkedin_oauth.ts` — reuse existing LinkedIn API helpers + +#### Task C: Connections Access Control for Guest Users + +**Purpose**: Enforce that guest users cannot access their connections until they upgrade their account (walkthrough Sections 10.2, 10.5, 10.6). + +**Options** (choose one): + +**Option 1 — Backend enforcement (recommended)**: +- Add middleware or guard check on ParticipantConnection query routes +- If `req.user.authProvider === 'guest'`, return `403 Forbidden` with message: `"Upgrade your account to access connections"` +- Affected routes: + - `GET /api/participantConnections/getByParticipantAndEvent` + - `GET /api/participantConnections/getByUserEmailAndEvent` +- Creating connections is still allowed (connections are made during gameplay), only reading is restricted + +**Option 2 — Frontend-only enforcement**: +- The user profile response already includes `authProvider` +- Frontend checks `authProvider === 'guest'` and shows the upgrade screen instead of connections +- No backend changes needed, but less secure (API still returns data if called directly) + +**Implementation** (Option 1): +- Add a helper function `requireUpgradedAccount` in `src/middleware/` or inline in the controller +- Check user's `authProvider` field from the database (or from JWT payload if added) +- Return 403 with a descriptive error message for guest users + +**Frontend Dependency**: Mobile app "Locked Connections" screen (walkthrough Section 10.5) routes to Account Upgrade Screen. + +--- + +## Phase 1: Event Model Updates & Status API (Critical Priority) + +These features are required for the mobile app to work without mock data. The mobile client depends on `gameType`, `eventImg`, and `currentState` enum values from the Event model, and polls for status transitions in the lobby. + +### 1.1 Add `gameType` and `eventImg` to Event Model ✅ COMPLETE + +**File**: `src/models/event_model.ts` + +**Changes**: +```typescript +gameType: { + type: String, + enum: ['Name Bingo'], + required: true +}, +eventImg: { + type: String, + required: false +} +``` + +**Why**: Mobile renders `gameType` and `eventImg` from event data. Without these fields, the mobile app uses hardcoded fallbacks. + +**Impact on existing endpoints**: +- `POST /api/events/createEvent` — accepts `gameType` (required) and `eventImg` (optional) in request body +- `GET /api/events/:eventId` and `GET /api/events/event/:joinCode` — automatically included in response + +**Frontend Dependency**: Mobile event cards display game type badge and event image. + +--- + +### 1.2 Add Event Status Enum to Event Model ✅ COMPLETE + +**File**: `src/models/event_model.ts` + +**Change**: +```typescript +// Before +currentState: { type: String, required: true } + +// After +currentState: { + type: String, + enum: ['Upcoming', 'In Progress', 'Completed'], + default: 'Upcoming', + required: true +} +``` + +**Important**: These enum values match the mobile app's `EventState` enum exactly (title case with spaces): +- `"Upcoming"` — event created but not started +- `"In Progress"` — event is live, bingo game active +- `"Completed"` — event has ended + +**Migration consideration**: Any existing events with free-form `currentState` values (e.g., `"pending"`, `"active"`) will need to be updated to match the new enum values. Check existing data before applying. + +--- + +### 1.3 Event Status Transition API ✅ COMPLETE + +**Endpoint**: `PUT /api/events/:eventId/status` + +**Request Body**: `{ "status": "In Progress" }` or `{ "status": "Completed" }` + +**Valid Transitions**: +- `Upcoming` → `In Progress` (host starts event) +- `In Progress` → `Completed` (host ends event) + +**Security**: +- Protected route (requires auth) +- Verifies `event.createdBy === req.user.userId` (host only) + +**Side Effects**: +- Emits Pusher `event-started` on channel `event-${eventId}` when transitioning to `In Progress` (payload: `{ status: 'In Progress' }`) +- Emits Pusher `event-ended` on channel `event-${eventId}` when transitioning to `Completed` (payload: `{ status: 'Completed' }`) + +**Implementation**: +- Handler in `src/controllers/event_controller.ts` (`updateEventStatus`) +- Route in `src/routes/event_routes.ts` +- Validates transition is allowed (rejects invalid transitions with 400) +- Returns updated event + +**Frontend Dependency**: Web dashboard Start/End buttons; mobile lobby polls for status change to transition into game. + +--- + +## Phase 2: Real-time Game Events (High Priority) + +### 2.1 Pusher Events for Game State + +| Event | Channel | Payload | Trigger | Status | +|-------|---------|---------|---------|--------| +| `event-started` | `event-${eventId}` | `{ status: 'In Progress' }` | Host starts event (Phase 1.3) | ✅ Complete | +| `event-ended` | `event-${eventId}` | `{ status: 'Completed' }` | Host ends event (Phase 1.3) | ✅ Complete | +| `bingo-achieved` | `event-${eventId}` | `{ participantId, name, type: 'line' \| 'blackout' }` | Player completes line/blackout (future, if server-side game state is added) | Planned | + +**Note**: `event-started` and `event-ended` were implemented as part of Phase 1.3. The `bingo-achieved` event is deferred until server-side game state tracking is implemented (Phase 6), since mobile currently handles win detection client-side. + +--- + +## Phase 3: User & Event Management (Medium Priority) + +### 3.1 User Profile APIs ✅ COMPLETE + +**GET Endpoint**: `GET /api/users/:userId` ✅ +- Returns user profile excluding `passwordHash` +- Protected route (requires auth) +- Located in `user_controller.ts` (`getUserById`) + +**PUT Endpoint**: `PUT /api/users/:userId` ✅ +- **Updatable Fields**: `name`, `email`, `password`, `bio`, `profilePhoto`, `socialLinks` +- **Security**: Protected, self-only (`req.user.userId === req.params.userId` → 403 otherwise) +- **Guest upgrade**: Setting a password upgrades `authProvider` from `'guest'` to `'local'` +- **Validation**: Email format + uniqueness, password >= 8 chars, name cannot be empty +- Located in `user_controller.ts` (`updateUser`), route in `user_route.ts` + +--- + +### 3.2 Leave Event API + +**Endpoint**: `POST /api/events/:eventId/leave` + +**Logic**: +1. Find participant for `req.user.userId` + `eventId` +2. Verify user is not the host (hosts must delete, not leave) +3. Remove participant from `Event.participantIds` +4. Delete `Participant` document +5. Remove event from `User.eventHistoryIds` +6. Emit Pusher `participant-left` event + +--- + +### 3.3 Delete/Cancel Event API + +**Endpoint**: `DELETE /api/events/:eventId` + +**Constraints**: +- Host only (`event.createdBy === req.user.userId`) +- Only when `status === 'Upcoming'` (before event starts) + +**Cascade**: +- Delete all `Participant` documents for this event +- Delete the `Bingo` document +- Remove event from all users' `eventHistoryIds` + +--- + +### 3.4 Event History API ✅ COMPLETE + +**Endpoint**: `GET /api/users/:userId/events` (protected) + +**Purpose**: MVP requirement from `feature_list.md` - "View Previous Events (static list of past events)" + +**Response**: +```json +{ + "success": true, + "events": [ + { + "_id": "665a...", + "name": "Tech Mixer", + "description": "Monthly networking event", + "joinCode": "12345678", + "startDate": "2025-02-01T18:00:00.000Z", + "endDate": "2025-02-01T21:00:00.000Z", + "currentState": "Completed", + "participantIds": [ + { "_id": "666b...", "name": "John Doe", "userId": "664f..." }, + { "_id": "666c...", "name": "Jane Smith", "userId": "664e..." } + ] + } + ] +} +``` + +**Implementation**: +- Populates `eventHistoryIds` from the User document with nested populate on `participantIds` +- Each event includes participant data (`name`, `userId`) to enable loading participant connections +- Located in `user_controller.ts` (`getUserEvents`) + +**Frontend Dependency**: Mobile app "Previous Events" tab (right navigation tab). + +--- + +### 3.5 Get Current Event API + +**Endpoint**: `GET /api/users/:userId/current-event` + +**Purpose**: MVP requirement - "View Current Event" feature + +**Response** (if user is in an active event): +```json +{ + "hasActiveEvent": true, + "event": { + "_id": "...", + "eventName": "Tech Mixer", + "status": "In Progress", + "joinCode": "ABC123", + "participantCount": 25, + "role": "participant" + } +} +``` + +**Response** (if no active event): +```json +{ + "hasActiveEvent": false, + "event": null +} +``` + +**Logic**: +1. Query `Participant` collection for user's participation +2. Join with `Event` collection +3. Filter for `status` in ['Upcoming', 'In Progress'] +4. Return most recent if multiple (edge case) + +**Security**: Protected route, user can only query their own current event + +**Frontend Dependency**: Mobile app needs this to determine if user should see event lobby or join screen. + +--- + +## Phase 4: Validation (Medium Priority) + +### 4.1 Zod Validation Schemas with Sanitization + +**File**: `src/validation/schemas.ts` + +**Schemas to Create**: +- `SignupSchema`, `LoginSchema` +- `CreateEventSchema`, `JoinEventSchema` +- `CreateBingoSchema` +- `UpdateProfileSchema` + +**Input Sanitization** (add to all string fields): +```typescript +import { z } from 'zod'; + +// Sanitization helper +const sanitizeString = (str: string) => str.trim(); + +// Example schema with sanitization +const SignupSchema = z.object({ + name: z.string().min(1).max(100).transform(sanitizeString), + email: z.string().email().transform(s => s.toLowerCase().trim()), + password: z.string().min(8).max(128) +}); +``` + +**Middleware**: `src/middleware/validate.ts` +```typescript +export const validate = (schema: ZodSchema) => (req, res, next) => { + const result = schema.safeParse(req.body); + if (!result.success) return res.status(400).json({ errors: result.error.issues }); + req.body = result.data; // Use sanitized data + next(); +}; +``` + +--- + +## Phase 5: Documentation Tasks ✅ COMPLETE + +All documentation has been created and is located in `shatter-backend/docs/`. + +### 5.1 API_REFERENCE.md ✅ COMPLETE +- Comprehensive endpoint documentation (1,164 lines) +- Covers all implemented endpoints with request/response examples, error codes, and auth requirements +- Located at `docs/API_REFERENCE.md` + +### 5.2 REALTIME_EVENTS_GUIDE.md ✅ COMPLETE +- Pusher setup, channel naming, event payloads, client-side examples +- Located at `docs/REALTIME_EVENTS_GUIDE.md` + +### 5.3 DATABASE_SCHEMA.md ✅ COMPLETE +- All collections with field definitions, indexes, relationships, and pre-save hooks +- Located at `docs/DATABASE_SCHEMA.md` + +### 5.4 EVENT_LIFECYCLE.md ✅ COMPLETE +- State diagram, transition rules, side effects, frontend integration notes +- Located at `docs/EVENT_LIFECYCLE.md` + +--- + +## Phase 6: Polish & Production Ready (Low Priority) + +These are P3 tasks that improve UX and performance but aren't blocking for MVP. Includes tasks deferred from earlier phases because mobile handles them client-side. + +### 6.1 Edit Bingo Square API + +**Endpoint**: Extend `POST /api/bingo/:bingoId/fill-cell` + +**Purpose**: Allow users to change already-filled cells (fix mistakes). + +**Logic**: +- If cell already filled, allow changing the assigned person +- Re-run line detection after change +- Could decrease `completedLines` if editing breaks a line +- Optional: Add event config `allowCellEditing: boolean` + +**Frontend Dependency**: Mobile app cell tap on filled cell shows edit option. + +--- + +### 6.2 Prevent Duplicate Person Assignments (Server-side) + +**Purpose**: Game rule validation (configurable per event). Mobile already handles this client-side for MVP. + +**Implementation**: +- Add to Bingo model: `allowDuplicateAssignments: boolean` (default: true) +- If false, validate `matchedParticipantId` not already used in another cell +- Return 400 error: "You've already assigned {name} to another square" + +--- + +### 6.3 Bingo Leaderboard API + +**Endpoint**: `GET /api/events/:eventId/bingo/leaderboard` + +**Purpose**: Show who completed lines/blackout first. Requires server-side game state (Phase 6.6). + +**Response**: +```json +{ + "leaderboard": [ + { + "rank": 1, + "participantId": "...", + "name": "Alice", + "linesCompleted": 3, + "blackoutAchieved": true, + "blackoutAt": "2024-01-15T14:30:00Z" + } + ] +} +``` + +**Logic**: +- Aggregate all `PlayerBingoState` for the event +- Sort by: `blackoutAchieved` (true first), then `blackoutAt` (earliest first), then `completedLines` (most first) + +**Frontend Dependency**: Optional leaderboard display during/after game. + +--- + +### 6.4 Database Indexes for Performance + +**Purpose**: Optimize queries for scale. + +**Indexes to Create**: + +| Collection | Index | Type | +|------------|-------|------| +| `events` | `joinCode` | unique | +| `events` | `currentState` | regular | +| `events` | `createdBy` | regular | +| `participants` | `(eventId, name)` | compound, unique (case-insensitive collation) | +| `participants` | `eventId` | regular | +| `users` | `email` | unique | +| `users` | `contactLink` | unique, sparse | +| `users` | `linkedinUrl` | unique, sparse | + +**Implementation**: Add to model definitions or create migration script. + +--- + +### 6.5 Participant Search API (Deferred from Phase 1) + +**Note**: Deferred because the mobile app gets participant names from the event's `participantIds` array (populated on `GET /api/events/:eventId`). A dedicated search endpoint is a nice-to-have for large events but not required for MVP. + +**Endpoint**: `GET /api/events/:eventId/participants/search` + +**Query Params**: `?name=` + +**Purpose**: Enable fuzzy name matching for the bingo cell-filling modal (see `bingo_walkthrough.md` Section 7.1). + +**Requirements**: +- Case-insensitive search on `Participant.name` +- Support partial matches (e.g., "joh" matches "John", "Johnny") +- Return max 10 results +- **Include profilePhoto in response** (per bingo_walkthrough 7.1) +- Response: `{ participants: [{ _id, name, profilePhoto }] }` + +**Implementation**: +- Create `src/controllers/participant_controller.ts` +- Create `src/routes/participant_routes.ts` +- Mount at `/api/participants` in `app.ts` +- Use MongoDB `$regex` with `'i'` flag for case-insensitive matching +- Join with User collection to get `profilePhoto` + +**Frontend Dependency**: Mobile app "Who did you find?" modal — currently uses in-memory filtering of participant list. + +--- + +### 6.6 PlayerBingoState Model (Deferred from Phase 1) + +**Note**: Deferred because mobile handles all bingo game state client-side via AsyncStorage for MVP. Server-side state tracking becomes important post-MVP for leaderboards, analytics, and cross-device sync. + +**File**: `src/models/player_bingo_state_model.ts` + +**Schema**: +```typescript +{ + eventId: ObjectId, // ref: Event + bingoId: string, // ref: Bingo + participantId: ObjectId, // ref: Participant (the player) + filledCells: [{ + row: number, + col: number, + matchedParticipantId: ObjectId | null, // null for free-text entries + matchedName: string, + filledAt: Date + }], + completedLines: number, + firstBingoAt: Date | null, + blackoutAt: Date | null, + isLocked: boolean +} +``` + +**Bingo Model Enhancements** (add to existing `bingo_model.ts`): +```typescript +{ + // Existing fields... + gridSize: { + type: Number, + enum: [3, 4, 5], + default: 5, + required: true + }, + prompts: [{ + text: String, // Full prompt text + shortText: String // Abbreviated version (e.g., "Has dog") + }], + allowFreeTextEntry: { + type: Boolean, + default: false // Allow typing names not in participant list + }, + allowDuplicateAssignments: { + type: Boolean, + default: true + } +} +``` + +**Indexes**: +- Compound unique index on `(eventId, participantId)` + +--- + +### 6.7 Cell Fill API (Deferred from Phase 1) + +**Note**: Deferred because mobile manages cell fills locally via AsyncStorage for MVP. + +**Endpoint**: `POST /api/bingo/:bingoId/fill-cell` + +**Request Body**: +```json +{ + "participantId": "player's participant ID", + "row": 0, + "col": 2, + "matchedParticipantId": "matched person's participant ID", + "matchedName": "John Doe" +} +``` + +**Logic**: +1. Validate player is in the event +2. If `matchedParticipantId` provided, validate matched participant exists in event +3. If `matchedParticipantId` is null, verify `allowFreeTextEntry` is true +4. Check cell not already filled +5. Update `PlayerBingoState.filledCells` +6. Run line detection (rows, columns, diagonals) +7. Check for blackout (all cells filled) +8. Record `firstBingoAt` on first line completion +9. Record `blackoutAt` on blackout +10. Emit Pusher event `bingo-achieved` if line/blackout + +--- + +### 6.8 Get Player Bingo State API (Deferred from Phase 1) + +**Note**: Deferred because mobile uses AsyncStorage for game state persistence for MVP. + +**Endpoint**: `GET /api/bingo/:bingoId/state/:participantId` + +**Response**: +```json +{ + "gridSize": 5, + "grid": [ + [{"text": "Has a dog", "shortText": "Has dog"}] + ], + "filledCells": [{ "row": 0, "col": 1, "matchedName": "John", "matchedParticipantId": "..." }], + "completedLines": 0, + "firstBingoAt": null, + "blackoutAt": null, + "isLocked": false, + "allowFreeTextEntry": false +} +``` + +--- + +### 6.9 Error Handling Middleware + +**File**: `src/middleware/error_handler.ts` + +**Purpose**: Consistent error responses across all endpoints. + +**Implementation**: +```typescript +export const errorHandler = (err, req, res, next) => { + console.error(err.stack); + + // Mongoose validation errors + if (err.name === 'ValidationError') { + return res.status(400).json({ + error: 'Validation failed', + details: Object.values(err.errors).map(e => e.message) + }); + } + + // Mongoose duplicate key + if (err.code === 11000) { + return res.status(409).json({ + error: 'Duplicate entry', + field: Object.keys(err.keyPattern)[0] + }); + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + return res.status(401).json({ error: 'Invalid token' }); + } + + // Default + res.status(500).json({ error: 'Internal server error' }); +}; +``` + +**Mount in `app.ts`**: `app.use(errorHandler)` after all routes. + +--- + +### 6.10 Rate Limiting Middleware + +**File**: `src/middleware/rate_limiter.ts` + +**Purpose**: Protect against brute force attacks and API abuse (feature_list.md "Wants") + +**Implementation**: +```typescript +import rateLimit from 'express-rate-limit'; + +// Strict limiter for auth endpoints +export const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // 10 requests per window + message: { error: 'Too many login attempts, please try again later' }, + standardHeaders: true, + legacyHeaders: false +}); + +// General API limiter +export const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + message: { error: 'Too many requests, please slow down' } +}); +``` + +**Installation**: +```bash +npm install express-rate-limit +npm install -D @types/express-rate-limit +``` + +--- + +### 6.11 Organizer Analytics Endpoints (Low Priority / Future) + +**Reference**: `feature_list.md` "Wants" section — "Organizer Analytics endpoints" + +**Purpose**: Provide organizers with event analytics such as attendance count, average engagement, and participation stats. + +**Potential Endpoints**: +- `GET /api/events/:eventId/analytics` — Returns event-level analytics (protected, host only) + - Participant count, bingo completion rates, average lines completed, blackout count + - Connection counts, most-connected participants + - Time-based metrics (average time to first bingo, event duration) + +**Implementation Notes**: +- Aggregate data from `Participant`, `PlayerBingoState`, and `ParticipantConnection` collections +- Consider caching analytics results for ended events (data won't change) +- Low priority — not blocking MVP + +**Frontend Dependency**: Web organizer dashboard "Event Summary Page" and analytics views. + +--- + +## Sprint Recommendations + +### Sprint 1: Event Model Updates & Status API ✅ COMPLETE +- ~~Task 1.1: Add `gameType` and `eventImg` to Event model~~ ✅ +- ~~Task 1.2: Add `currentState` enum (`Upcoming`, `In Progress`, `Completed`)~~ ✅ +- ~~Task 1.3: Event Status Transition API (`PUT /api/events/:eventId/status`)~~ ✅ + +### Sprint 2: Guest Account Upgrades & Real-time +- Task 0.3B: LinkedIn Account Linking for Guest Users +- Task 0.3C: Connections Access Control for Guests +- ~~Task 2.1: Pusher game events (`event-started`, `event-ended`)~~ ✅ Implemented as part of Phase 1.3 + +### Sprint 3: Event Management +- ~~Task 3.1: User Profile APIs (GET and PUT)~~ ✅ Complete +- Task 3.2: Leave Event API +- Task 3.3: Delete/Cancel Event API +- ~~Task 3.4: Event History API~~ ✅ Complete +- Task 3.5: Get Current Event API + +### Sprint 4: Validation +- Task 4.1: Zod Validation Schemas with Sanitization +- ~~Documentation tasks (5.1-5.4)~~ ✅ Complete + +### Sprint 5: Server-side Game State & Polish (if needed post-MVP) +- Task 6.5: Participant Search API +- Task 6.6: PlayerBingoState Model +- Task 6.7: Cell Fill API +- Task 6.8: Get Player Bingo State API +- Task 6.1: Edit Bingo Square +- Task 6.2: Prevent Duplicate Assignments (server-side) +- Task 6.3: Bingo Leaderboard +- Task 6.4: Database Indexes +- Task 6.9: Error Handling Middleware +- Task 6.10: Rate Limiting Middleware +- Task 6.11: Organizer Analytics Endpoints + +--- + +## Dependencies to Install + +```bash +# Authentication - Already installed: bcryptjs, jsonwebtoken, axios (for LinkedIn OAuth) +# No passport needed - LinkedIn OAuth uses custom implementation with axios + +# Validation +npm install zod + +# Rate Limiting +npm install express-rate-limit +npm install -D @types/express-rate-limit +``` + +--- + +## Data Model Summary + +### User Collection +```typescript +{ + _id: ObjectId, + name: string, + email?: string (unique, sparse index), // optional for guest users + passwordHash?: string (select: false), + linkedinId?: string (unique, sparse index), // LinkedIn subject ID + linkedinUrl?: string (unique, sparse index), + authProvider: 'local' | 'linkedin' | 'guest' (default: 'local'), + bio?: string, + profilePhoto?: string, + socialLinks?: { + linkedin?: string, + github?: string, + other?: string + }, + lastLogin?: Date, + passwordChangedAt?: Date, + eventHistoryIds: ObjectId[], + createdAt: Date, + updatedAt: Date +} +``` + +### AuthCode Collection (temporary, auto-expiring) +```typescript +{ + _id: ObjectId, + code: string (unique, indexed), + userId: ObjectId (ref: User), + createdAt: Date (TTL: 60 seconds) +} +``` + +### Event Collection +```typescript +{ + _id: ObjectId, + name: string, + description: string, + createdBy: ObjectId (ref: User), + joinCode: string (unique), + currentState: 'Upcoming' | 'In Progress' | 'Completed' (default: 'Upcoming'), + gameType: 'Name Bingo' (required), + eventImg?: string, + startDate: Date, + endDate: Date, + maxParticipant: number, + participantIds: ObjectId[] (ref: Participant), + createdAt: Date, + updatedAt: Date +} +``` + +### Participant Collection +```typescript +{ + _id: ObjectId, + eventId: ObjectId (ref: Event, required), + userId: ObjectId | null (ref: User, default: null), // nullable, not required + name: string (required) +} +// Index: (eventId, name) compound unique with case-insensitive collation +// Note: duplicate names get an automatic #XXX suffix (e.g., "John#472") instead of being rejected +// Note: no role or joinedAt fields in current model +``` + +### Bingo Collection +```typescript +{ + _id: string (auto-generated, e.g. "bingo_xxxxxxxx"), + _eventId: ObjectId (ref: Event, required), + description?: string, + grid?: string[][] (2D string array) +} +// Note: gridSize, prompts, allowFreeTextEntry, allowDuplicateAssignments are planned for Phase 6 +``` + +### ParticipantConnection Collection ✅ IMPLEMENTED +```typescript +{ + _id: string (auto-generated, e.g. "participantConnection_xxxxxxxx"), + _eventId: ObjectId (ref: Event, required), + primaryParticipantId: ObjectId (ref: Participant, required), + secondaryParticipantId: ObjectId (ref: Participant, required), + description?: string +} +// Duplicate prevention: checked via query on (_eventId, primaryParticipantId, secondaryParticipantId) +``` + +### PlayerBingoState Collection (Planned — Phase 6) +```typescript +{ + _id: ObjectId, + eventId: ObjectId (ref: Event), + bingoId: ObjectId (ref: Bingo), + participantId: ObjectId (ref: Participant), + filledCells: [{ + row: number, + col: number, + matchedParticipantId: ObjectId | null, + matchedName: string, + filledAt: Date + }], + completedLines: number, + firstBingoAt: Date | null, + blackoutAt: Date | null, + isLocked: boolean +} +// Index: (eventId, participantId) compound unique +``` + +--- + +## API Summary + +### Authentication +``` +POST /api/auth/signup - Email/password OR contact link signup +POST /api/auth/login - Email/password login +GET /api/auth/linkedin - Initiate LinkedIn OAuth ✅ +GET /api/auth/linkedin/callback - LinkedIn OAuth callback ✅ +POST /api/auth/exchange - Exchange auth code for JWT ✅ +POST /api/auth/linkedin/link - Link LinkedIn to existing guest account (protected, guest only) +GET /api/users/me - Get current user (protected) +``` + +### Users +``` +GET /api/users/:userId - Get user profile ✅ +PUT /api/users/:userId - Update user profile (protected, self only) ✅ +GET /api/users/:userId/events - Get user's event history (protected) ✅ +GET /api/users/:userId/current-event - Get user's active event (protected) +``` + +### Events +``` +POST /api/events - Create event (protected) +POST /api/events/join - Join event with joinCode (protected) +GET /api/events/:eventId - Get event details +PUT /api/events/:eventId/status - Update event status (protected, host only) +POST /api/events/:eventId/leave - Leave event (protected) +DELETE /api/events/:eventId - Cancel event (protected, host only, Upcoming only) +GET /api/events/:eventId/participants/search - Search participants (protected, Phase 6) +``` + +### Participant Connections ✅ IMPLEMENTED +``` +POST /api/participantConnections - Create connection by participant IDs (protected) ✅ +POST /api/participantConnections/by-emails - Create connection by user emails (protected) ✅ +DELETE /api/participantConnections/delete - Delete connection (protected) ✅ +GET /api/participantConnections/getByParticipantAndEvent - Get connections by participant & event (protected) ✅ +GET /api/participantConnections/getByUserEmailAndEvent - Get connections by user email & event (protected) ✅ +``` + +### Name Bingo (Phase 6 — server-side game state) +``` +GET /api/bingo/:bingoId/state/:participantId - Get player's board state (protected) +POST /api/bingo/:bingoId/fill-cell - Fill a cell (protected) +GET /api/events/:eventId/bingo/leaderboard - Get leaderboard (protected) +``` diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 7d23c0c..dd8baff 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -57,8 +57,13 @@ export const signup = async (req: Request, res: Response) => { // check if email already exists const existingUser = await User.findOne({ email: normalizedEmail }).lean(); if (existingUser) { + if (existingUser.authProvider === 'linkedin') { + return res.status(409).json({ + error: 'This email is associated with a LinkedIn account. Please log in with LinkedIn.', + }); + } return res.status(409).json({ - error: 'Email already exists' + error: 'Email already exists', }); } diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index c2c0c91..075a220 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -6,10 +6,44 @@ import "../models/participant_model"; import { generateJoinCode } from "../utils/event_utils"; import { generateToken } from "../utils/jwt_utils"; -import { Participant } from "../models/participant_model"; +import { Participant, IParticipant } from "../models/participant_model"; import { User } from "../models/user_model"; import { Types } from "mongoose"; +/** + * Create a participant with automatic name suffix on collision. + * If the name already exists in the event, retries with a random #XXX suffix. + */ +async function createParticipantWithRetry( + userId: Types.ObjectId | null, + name: string, + eventId: string, + maxRetries: number = 5 +): Promise<{ participant: IParticipant; finalName: string }> { + let finalName = name; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const participant = await Participant.create({ + userId, + name: finalName, + eventId, + }); + return { participant, finalName }; + } catch (e: any) { + if (e.code === 11000 && e.keyPattern?.name && e.keyPattern?.eventId) { + const suffix = String(Math.floor(Math.random() * 999) + 1).padStart( + 3, + "0" + ); + finalName = `${name}#${suffix}`; + continue; + } + throw e; + } + } + throw { code: 11000, keyPattern: { name: 1, eventId: 1 } }; +} + /** * POST /api/events/createEvent * Create a new event @@ -159,22 +193,22 @@ export async function joinEventAsUser(req: Request, res: Response) { if (event.participantIds.length >= event.maxParticipant) return res.status(400).json({ success: false, msg: "Event is full" }); - let participant = await Participant.findOne({ + const existingParticipant = await Participant.findOne({ userId, eventId, }); - if (participant) { + if (existingParticipant) { return res .status(409) .json({ success: false, msg: "User already joined" }); } - participant = await Participant.create({ + const { participant, finalName } = await createParticipantWithRetry( userId, name, eventId, - }); + ); const participantId = participant._id as Types.ObjectId; @@ -196,14 +230,14 @@ export async function joinEventAsUser(req: Request, res: Response) { ); console.log("Room socket:", eventId); - console.log("Participant data:", { participantId, name }); + console.log("Participant data:", { participantId, name: finalName }); await pusher.trigger( `event-${eventId}`, // channel (room) "participant-joined", // event name { participantId, - name, + name: finalName, }, ); @@ -213,12 +247,6 @@ export async function joinEventAsUser(req: Request, res: Response) { }); } catch (e: any) { if (e.code === 11000) { - if (e.keyPattern?.name && e.keyPattern?.eventId) { - return res.status(409).json({ - success: false, - msg: "This name is already taken in this event", - }); - } if (e.keyPattern?.email) { return res.status(409).json({ success: false, @@ -272,12 +300,17 @@ export async function joinEventAsGuest(req: Request, res: Response) { const userId = user._id as Types.ObjectId; const token = generateToken(userId.toString()); - // Create participant linked to the new user - const participant = await Participant.create({ + // Create participant linked to the new user, with automatic #XXX suffix on name collision + const { participant, finalName } = await createParticipantWithRetry( userId, name, eventId, - }); + ); + + // Update guest user's name to match the suffixed participant name + if (finalName !== name) { + await User.updateOne({ _id: userId }, { name: finalName }); + } const participantId = participant._id as Types.ObjectId; @@ -293,14 +326,14 @@ export async function joinEventAsGuest(req: Request, res: Response) { // Emit socket console.log("Room socket:", eventId); - console.log("Participant data:", { participantId, name }); + console.log("Participant data:", { participantId, name: finalName }); await pusher.trigger( `event-${eventId}`, // channel (room) "participant-joined", // event name { participantId, - name, + name: finalName, }, ); @@ -312,12 +345,6 @@ export async function joinEventAsGuest(req: Request, res: Response) { }); } catch (e: any) { if (e.code === 11000) { - if (e.keyPattern?.name && e.keyPattern?.eventId) { - return res.status(409).json({ - success: false, - msg: "This name is already taken in this event", - }); - } if (e.keyPattern?.email) { return res.status(409).json({ success: false, diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 55e745b..a9a73fa 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import { User } from "../models/user_model"; +import "../models/participant_model"; import { hashPassword } from "../utils/password_hash"; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; @@ -76,7 +77,14 @@ export const getUserEvents = async (req: Request, res: Response) => { } const user = await User.findById(userId) - .populate("eventHistoryIds", "name description joinCode startDate endDate currentState") + .populate({ + path: "eventHistoryIds", + select: "name description joinCode startDate endDate currentState participantIds", + populate: { + path: "participantIds", + select: "name userId", + }, + }) .select("eventHistoryIds"); if (!user) { From 0d40d33cfea4870ac3a34bbaa76e017697ca21d6 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Mon, 9 Mar 2026 17:11:06 -0600 Subject: [PATCH 04/79] Getting a working script progress --- shatter-backend/src/ai/chatgpt.ts | 259 +++++++++++++++++++++++ shatter-backend/src/ai/gemini.ts | 224 ++++++++++++++------ shatter-backend/src/ai/prompt_builder.ts | 2 +- shatter-backend/src/ai/prompts/bingo.txt | 17 +- 4 files changed, 426 insertions(+), 76 deletions(-) create mode 100644 shatter-backend/src/ai/chatgpt.ts diff --git a/shatter-backend/src/ai/chatgpt.ts b/shatter-backend/src/ai/chatgpt.ts new file mode 100644 index 0000000..29dff1f --- /dev/null +++ b/shatter-backend/src/ai/chatgpt.ts @@ -0,0 +1,259 @@ +// src/ai/chatgpt.ts + +import OpenAI from "openai"; +import "dotenv/config"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Prompt } from "./prompt_builder.js"; + +console.log("===== OpenAI Bingo Generator Starting ====="); + +// ---------------------- +// File path setup +// ---------------------- + +console.log("Resolving file paths..."); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +console.log("Current file:", __filename); +console.log("Current directory:", __dirname); + +const promptPath = path.join(__dirname, "prompts", "bingo.txt"); + +console.log("Prompt path:", promptPath); +console.log("Prompt exists:", fs.existsSync(promptPath)); + +// ---------------------- +// Helpers +// ---------------------- + +function makeEmptyGrid(rows: number, cols: number): string[][] { + console.log(`Creating fallback empty grid ${rows}x${cols}`); + return Array.from({ length: rows }, () => Array(cols).fill("")); +} + +function buildShapeExample(rows: number, cols: number): string { + console.log("Building shape example..."); + + const rowsText = Array.from({ length: rows }, (_, r) => { + const colsText = Array.from( + { length: cols }, + (_, c) => `"Row ${r + 1} Col ${c + 1}"` + ); + return ` [${colsText.join(", ")}]`; + }); + + const example = `{ + "grid": [ +${rowsText.join(",\n")} + ] +}`; + + console.log("Shape example:"); + console.log(example); + + return example; +} + +function buildJsonSchema(rows: number, cols: number) { + console.log("Constructing JSON schema..."); + + const schema = { + type: "object", + additionalProperties: false, + properties: { + grid: { + type: "array", + minItems: rows, + maxItems: rows, + items: { + type: "array", + minItems: cols, + maxItems: cols, + items: { + type: "string" + } + } + } + }, + required: ["grid"] + }; + + console.log("JSON schema created:"); + console.log(JSON.stringify(schema, null, 2)); + + return schema; +} + +// ---------------------- +// Types +// ---------------------- + +export type Bingo = { + grid: string[][]; +}; + +// ---------------------- +// OpenAI Setup +// ---------------------- + +console.log("Initializing OpenAI client..."); + +const client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY +}); + +console.log( + "API key loaded:", + process.env.OPENAI_API_KEY ? "YES" : "NO" +); + +// ---------------------- +// Generator +// ---------------------- + +export async function generateNameBingo( + rows: number, + cols: number +): Promise { + + console.log("\n===== generateNameBingo START ====="); + + console.log("Rows:", rows); + console.log("Cols:", cols); + + if (rows <= 0 || cols <= 0) { + console.log("Invalid grid size"); + return { grid: [[]] }; + } + + // ---------------------- + // Schema + // ---------------------- + + const jsonSchema = buildJsonSchema(rows, cols); + + // ---------------------- + // Prompt + // ---------------------- + + const shapeExample = buildShapeExample(rows, cols); + + const basePrompt = ` +Generate a ${rows}x${cols} bingo grid. + +Return JSON matching this exact structure: + +${shapeExample} + +Rules: +- Every cell should contain a short phrase +- Make entries playful and tech workplace related +`.trim(); + + const preContext = + "The following is the context of the event where this bingo game will be played:"; + + const eventContext = + "This is a team-building event for a software development company. Attendees include engineers, designers, and product managers."; + + console.log("Loading instruction file..."); + + if (!fs.existsSync(promptPath)) { + throw new Error(`Prompt file missing: ${promptPath}`); + } + + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); + + console.log("Instruction file loaded."); + console.log("Instruction length:", aiInstruction.length); + + console.log("Building prompt..."); + + const aiPrompt = new Prompt([ + basePrompt, + preContext, + eventContext, + aiInstruction + ]); + + aiPrompt.generatePrompt(); + const prompt = aiPrompt.getPrompt(); + + console.log("\n===== FINAL PROMPT ====="); + console.log(prompt); + console.log("========================\n"); + + // ---------------------- + // OpenAI Request + // ---------------------- + + try { + + console.log("Sending request to OpenAI..."); + + const response = await client.responses.create({ + model: "gpt-5.2", + temperature: 0.7, + input: prompt, + + text: { + format: { + type: "json_schema", + name: "bingo_grid", + schema: jsonSchema + } + } + }); + + console.log("Response received from OpenAI"); + + console.log("Full raw response:"); + console.log(JSON.stringify(response, null, 2)); + + const json = + response.output?.[0]?.content?.[0]?.json; + + console.log("Extracted JSON:"); + console.log(JSON.stringify(json, null, 2)); + + console.log("Returning generated grid"); + + return json; + + } catch (error) { + + console.error("\n===== OPENAI GENERATION FAILED ====="); + console.error(error); + + console.log("Returning fallback grid"); + + return { grid: makeEmptyGrid(rows, cols) }; + } +} + +// ---------------------- +// Run directly +// ---------------------- + +async function main() { + + console.log("\n===== MAIN START ====="); + + const result = await generateNameBingo(4, 4); + + console.log("\n===== FINAL RESULT ====="); + + console.log(JSON.stringify(result, null, 2)); + + console.log("\n===== PROGRAM COMPLETE ====="); +} + +main().catch((err) => { + console.error("Fatal error:"); + console.error(err); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/shatter-backend/src/ai/gemini.ts b/shatter-backend/src/ai/gemini.ts index 4fb39f0..b2e71e2 100644 --- a/shatter-backend/src/ai/gemini.ts +++ b/shatter-backend/src/ai/gemini.ts @@ -1,128 +1,218 @@ // src/ai/gemini.ts + import { GoogleGenAI } from "@google/genai"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import "dotenv/config"; + import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; -// ---------------------- -// Schema (1D array of names) -// ---------------------- -const bingoSchema = z.object({ - grid: z.array(z.string()), -}); +import { Prompt } from "./prompt_builder.ts"; + +console.log("Gemini module starting..."); -export type Bingo = z.infer; // ---------------------- -// AI Setup +// File path setup // ---------------------- -const ai = new GoogleGenAI({ - apiKey: process.env.GOOGLE_API_KEY, -}); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const promptPath = path.join(__dirname, "prompts", "bingo.txt"); + +console.log("Resolved prompt path:", promptPath); +console.log("Prompt file exists:", fs.existsSync(promptPath)); + // ---------------------- // Helpers // ---------------------- -function makeEmptyGrid(size: number): string[] { - return Array.from({ length: size }, () => ""); + +function makeEmptyGrid(rows: number, cols: number) { + + console.log(`Creating fallback grid ${rows}x${cols}`); + + const grid: Record = {}; + + for (let r = 1; r <= rows; r++) { + grid[`row${r}`] = Array(cols).fill(""); + } + + return grid; } -function extractJsonObject(text: string): string { - const trimmed = text.trim(); - if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed; - const first = trimmed.indexOf("{"); - const last = trimmed.lastIndexOf("}"); - if (first !== -1 && last !== -1 && last > first) { - return trimmed.slice(first, last + 1); +function buildSchema(rows: number, cols: number) { + + console.log("Building dynamic Zod schema..."); + + const shape: Record = {}; + + for (let r = 1; r <= rows; r++) { + + shape[`row${r}`] = z + .array( + z + .string() + .describe("A humorous bingo square phrase related to tech culture") + ) + .length(cols) + .describe(`Row ${r} of the bingo board`); } - return trimmed; + return z.object(shape); } -function readPromptTemplate(): string { - // Resolves relative to this file’s location reliably - const filePath = path.resolve(process.cwd(), "src", "ai", "prompts", "bingo.txt"); - return fs.readFileSync(filePath, "utf8"); + +function buildShapeExample(rows: number, cols: number) { + + console.log("Building JSON example for prompt"); + + const obj: Record = {}; + + for (let r = 1; r <= rows; r++) { + obj[`row${r}`] = Array.from( + { length: cols }, + (_, c) => `Row ${r} Col ${c + 1}` + ); + } + + return JSON.stringify(obj, null, 2); } -function buildPromptFromTemplate(template: string, totalCells: number): string { - const schemaJson = JSON.stringify(zodToJsonSchema(bingoSchema), null, 2); - const emptyGridJson = JSON.stringify(makeEmptyGrid(totalCells)); - - // Supported placeholders in bingo.txt: - // {{TOTAL_CELLS}} -> total number of names to generate - // {{SCHEMA}} -> JSON schema - // {{EMPTY_GRID}} -> JSON array fallback for grid - return template - .replace(/{{TOTAL_CELLS}}/g, String(totalCells)) - .replace(/{{SCHEMA}}/g, schemaJson) - .replace(/{{EMPTY_GRID}}/g, emptyGridJson) - .trim(); + +// ---------------------- +// Gemini Client +// ---------------------- + +console.log("Initializing Gemini client..."); + +const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY; + +if (!apiKey) { + throw new Error("Missing API key"); } +const ai = new GoogleGenAI({ apiKey }); + +console.log("Gemini client initialized."); + + // ---------------------- // Generator // ---------------------- -export async function generateNameBingo(n_rows: number, n_cols: number): Promise { - const totalCells = n_rows * n_cols; - const template = readPromptTemplate(); - const prompt = buildPromptFromTemplate(template, totalCells); +export async function generateNameBingo(rows: number, cols: number) { + + console.log("generateNameBingo called"); + console.log("Grid size:", rows, "x", cols); + + const schema = buildSchema(rows, cols); + const schemaJson = zodToJsonSchema(schema); + + const example = buildShapeExample(rows, cols); + + const basePrompt = ` +Generate a ${rows}x${cols} bingo board. + +Return JSON exactly matching this structure: + +${example} + +Rules: +- Keys must be row1, row2, row3, etc. +- Each row must contain ${cols} strings. +- Each string is a humorous bingo phrase about software development culture. + +Return ONLY valid JSON. +`.trim(); + + + console.log("Loading instruction file..."); + + if (!fs.existsSync(promptPath)) { + throw new Error(`Prompt file missing: ${promptPath}`); + } + + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); + + const aiPrompt = new Prompt([ + basePrompt, + aiInstruction, + ]); + + console.log("Generating final prompt..."); + + aiPrompt.generatePrompt(); + + const prompt = aiPrompt.getPrompt(); + + console.log("Final prompt length:", prompt.length); - let rawResponse: string | undefined; try { + + console.log("Sending request to Gemini..."); + const response = await ai.models.generateContent({ - model: "gemini-3-flash-preview", + model: "gemini-2.5-flash", contents: prompt, config: { responseMimeType: "application/json", - responseJsonSchema: zodToJsonSchema(bingoSchema), + responseJsonSchema: schemaJson, temperature: 0.7, }, }); - rawResponse = typeof response.text === "string" ? response.text : String(response.text); + console.log("Gemini response received"); + + console.log("RAW RESPONSE:"); + console.log(response.text); + + + console.log("Parsing JSON..."); + + const parsed = JSON.parse(response.text); - const jsonText = extractJsonObject(rawResponse); - const parsed = JSON.parse(jsonText); - const validated = bingoSchema.parse(parsed); + console.log("Validating schema..."); - // Enforce exact length; fallback if wrong - if (validated.grid.length !== totalCells) { - return { grid: makeEmptyGrid(totalCells) }; - } + const validated = schema.parse(parsed); + + console.log("Schema validation successful"); return validated; - } catch (error) { - console.error("\n❌ Generation Failed\n"); - if (rawResponse) { - console.error("---- Raw Gemini Response ----"); - console.error(rawResponse); - console.error("-----------------------------\n"); - } else { - console.error("No response text was received from Gemini.\n"); - } + } catch (error) { - console.error("Error:", error); + console.error("Generation failed:", error); - return { grid: makeEmptyGrid(totalCells) }; + return makeEmptyGrid(rows, cols); } } + // ---------------------- // Run directly // ---------------------- + async function main() { + + console.log("Generating Bingo Grid..."); + const result = await generateNameBingo(4, 4); - console.log("\n✅ Generated Bingo Grid:\n"); + + console.log("Result:"); + console.log(JSON.stringify(result, null, 2)); } -main().catch(() => { + +main().catch((err) => { + + console.error("Fatal error:", err); + process.exitCode = 1; -}); +}); \ No newline at end of file diff --git a/shatter-backend/src/ai/prompt_builder.ts b/shatter-backend/src/ai/prompt_builder.ts index 578e8e6..c7cb6cc 100644 --- a/shatter-backend/src/ai/prompt_builder.ts +++ b/shatter-backend/src/ai/prompt_builder.ts @@ -4,7 +4,7 @@ * Allows incremental construction of a prompt string * from multiple parts, joined by a configurable separator. */ -class Prompt { +export class Prompt { private promptParts: string[]; private separator: string; diff --git a/shatter-backend/src/ai/prompts/bingo.txt b/shatter-backend/src/ai/prompts/bingo.txt index d5a9c2f..a0941a6 100644 --- a/shatter-backend/src/ai/prompts/bingo.txt +++ b/shatter-backend/src/ai/prompts/bingo.txt @@ -1,9 +1,10 @@ -Generate a Name Bingo game. +Generate creative bingo entries based on the provided event context. -Requirements: -- Return ONLY valid JSON (no markdown, no commentary). -- The ONLY top-level field must be "grid". -- "grid" must be a 1D array of exactly {{TOTAL_CELLS}} strings. -- Each element must be a RANDOM full name (first + last). -- All names should be unique. -- If you cannot generate valid JSON, return {"grid": {{EMPTY_GRID}} }. +Content guidelines: +- Each entry should be short and fit naturally in one bingo square. +- Make entries playful, workplace-relevant, and easy to recognize. +- Favor software team culture, meetings, product/design/engineering habits, and tech workplace jokes. +- Keep the tone lighthearted and suitable for a professional team event. +- Avoid generic party prompts. +- Avoid offensive, overly personal, or inappropriate content. +- Make all entries distinct from each other. \ No newline at end of file From b3cad67c13f8d050f5b05d60b63dc26a0e08b503 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:40:37 -0600 Subject: [PATCH 05/79] fix create event in frontend --- shatter-web/src/pages/DashboardPage.tsx | 8 ++++---- shatter-web/src/pages/EventPage.tsx | 4 ++-- shatter-web/src/service/CreateEvent.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/shatter-web/src/pages/DashboardPage.tsx b/shatter-web/src/pages/DashboardPage.tsx index 2aa0be0..42f6425 100644 --- a/shatter-web/src/pages/DashboardPage.tsx +++ b/shatter-web/src/pages/DashboardPage.tsx @@ -45,7 +45,7 @@ function DashboardPage() { startDate: "", endDate: "", maxParticipant: 0, - currentState: "upcoming", + currentState: "Upcoming", }); const [selectedIcebreaker, setSelectedIcebreaker] = useState(null); @@ -192,7 +192,7 @@ function DashboardPage() { startDate: selectedEvent.startDate?.substring(0, 16) || "", endDate: selectedEvent.endDate?.substring(0, 16) || "", maxParticipant: selectedEvent.maxParticipant || 0, - currentState: selectedEvent.currentState || "upcoming", + currentState: selectedEvent.currentState || "Upcoming", }); }; @@ -338,7 +338,7 @@ function DashboardPage() { switch (state) { case "ongoing": return { bg: "rgba(34, 197, 94, 0.2)", text: "#4ade80", border: "rgba(34, 197, 94, 0.3)" }; - case "upcoming": + case "Upcoming": return { bg: "rgba(59, 130, 246, 0.2)", text: "#60a5fa", border: "rgba(59, 130, 246, 0.3)" }; case "completed": return { bg: "rgba(156, 163, 175, 0.2)", text: "#9ca3af", border: "rgba(156, 163, 175, 0.3)" }; @@ -571,7 +571,7 @@ function DashboardPage() { onChange={(e) => setEditForm({ ...editForm, currentState: e.target.value })} className="w-full p-3 rounded-lg bg-white/5 border border-white/20 text-white focus:outline-none focus:border-[#4DC4FF] transition-colors font-body" > - + diff --git a/shatter-web/src/pages/EventPage.tsx b/shatter-web/src/pages/EventPage.tsx index 93c350f..b92072e 100644 --- a/shatter-web/src/pages/EventPage.tsx +++ b/shatter-web/src/pages/EventPage.tsx @@ -136,13 +136,13 @@ export default function EventPage() { backgroundColor: eventDetails.currentState === "ongoing" ? "rgba(34, 197, 94, 0.2)" - : eventDetails.currentState === "upcoming" + : eventDetails.currentState === "Upcoming" ? "rgba(59, 130, 246, 0.2)" : "rgba(156, 163, 175, 0.2)", color: eventDetails.currentState === "ongoing" ? "#4ade80" - : eventDetails.currentState === "upcoming" + : eventDetails.currentState === "Upcoming" ? "#60a5fa" : "#9ca3af", }} diff --git a/shatter-web/src/service/CreateEvent.ts b/shatter-web/src/service/CreateEvent.ts index 0725057..18726cb 100644 --- a/shatter-web/src/service/CreateEvent.ts +++ b/shatter-web/src/service/CreateEvent.ts @@ -25,7 +25,8 @@ export async function CreateEvent(eventData: { startDate, endDate, maxParticipant: eventData.maxParticipants, - currentState: 'upcoming', + currentState: 'Upcoming', + gameType: 'Name Bingo', }; const apiUrl = `${import.meta.env.VITE_API_URL}/events/createEvent`; From b8e424f9eadedc93be0b288eff5c8094b57ae54d Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Thu, 12 Mar 2026 02:24:03 -0600 Subject: [PATCH 06/79] Created bingo controller still need to test it. --- shatter-backend/src/ai/gemini.ts | 4 +- .../src/controllers/bingo_controller.ts | 232 ++++++++++++++++++ 2 files changed, 234 insertions(+), 2 deletions(-) diff --git a/shatter-backend/src/ai/gemini.ts b/shatter-backend/src/ai/gemini.ts index b2e71e2..4168146 100644 --- a/shatter-backend/src/ai/gemini.ts +++ b/shatter-backend/src/ai/gemini.ts @@ -90,7 +90,7 @@ function buildShapeExample(rows: number, cols: number) { console.log("Initializing Gemini client..."); -const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY; +const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) { throw new Error("Missing API key"); @@ -202,7 +202,7 @@ async function main() { console.log("Generating Bingo Grid..."); - const result = await generateNameBingo(4, 4); + const result = await generateNameBingo(3, 2); console.log("Result:"); diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index e094962..adfc57b 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -3,6 +3,17 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; import { Bingo } from "../models/bingo_model"; import { Event } from "../models/event_model"; +import { Prompt } from "../ai/prompt_builder"; + +import { GoogleGenAI } from "@google/genai"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import "dotenv/config"; + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Console } from "node:console"; /** * POST /api/bingo @@ -72,6 +83,9 @@ export async function createBingo(req: Request, res: Response) { } } +/** + * @param req.body.eventId - Bingo for a given eventId (string) (required) + */ export async function getBingo(req: Request, res: Response) { try { const { eventId } = req.params; @@ -177,4 +191,222 @@ export async function updateBingo(req: Request, res: Response) { } catch (err: any) { return res.status(500).json({ success: false, error: err.message }); } +} + + + +function makeEmptyGrid(rows: number, cols: number): Record { + + console.log(`Creating fallback grid ${rows}x${cols}`); + + const grid: Record = {}; + + for (let r = 1; r <= rows; r++) { + grid[`row${r}`] = Array(cols).fill(""); + } + + return grid; +} + +function buildSchema(rows: number, cols: number): Object { + + console.log("Building dynamic Zod schema..."); + + const shape: Record = {}; + + for (let r = 1; r <= rows; r++) { + + shape[`row${r}`] = z + .array( + z + .string() + .describe("A humorous bingo square phrase related to tech culture") + ) + .length(cols) + .describe(`Row ${r} of the bingo board`); + } + + return z.object(shape); +} + +function buildShapeExample(rows: number, cols: number): string { + + console.log("Building JSON example for prompt"); + + const obj: Record = {}; + + for (let r = 1; r <= rows; r++) { + obj[`row${r}`] = Array.from( + { length: cols }, + (_, c) => `Row ${r} Col ${c + 1}` + ); + } + + return JSON.stringify(obj, null, 2); +} + +/** + * Calls Gemini to generate a bingo grid + */ +async function generateBingoGrid(n_rows: number, n_cols: number, topic: string): Promise> { + + const schema = buildSchema(n_rows, n_cols); + const schemaJson = zodToJsonSchema(schema); + const example = buildShapeExample(n_rows, n_cols); + + const basePrompt = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: + ${example} + Rules: + - Keys must be row1, row2, row3, etc. + - Each row must contain ${n_cols} strings. + - Each string is a humorous bingo phrase about software development culture. + Return ONLY valid JSON. + `.trim(); + const promptPath = "../ai/prompts/bingo.txt"; + + if (!fs.existsSync(promptPath)) { + throw new Error(`Prompt file missing: ${promptPath}`); + } + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); + + const aiPrompt = new Prompt([basePrompt, aiInstruction]) + + aiPrompt.generatePrompt(); + + const prompt = aiPrompt.getPrompt(); + console.log("----------- Final Prompt -----------"); + console.log(prompt); + console.log("------------------------------------"); + + try { + + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: schemaJson, + temperature: 0.7, + }, + }); + + console.log("RAW RESPONSE:"); + console.log(response.text); + + const parsed = JSON.parse(response.text); + const validated: Record = schema.parse(parsed); + return validated; + + } catch (error) { + console.error("Generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) + } +} + +const apiKey = process.env.GEMINI_API_KEY; +const ai = new GoogleGenAI({ apiKey }); + +function process_ai_result(ai_result: Record) { + let grid: string[][] = []; + + const keys: string[] = Object.keys(ai_result); + for(let i = 0; i < keys.length; i++) { + let val: string[] = ai_result[keys[i]]; + grid.push(val) + } + + return grid; +} + +/** + * POST /api/bingo/generate + * + * Generate an AI bingo grid for an event. + * + * @param req.body._eventId - Event ObjectId (required) + * @param req.body.description - Description string (required but not used) + * @param req.body.topic - Topic for bingo content (required) + * @param req.body.n_rows - Number of grid rows (1-5) + * @param req.body.n_cols - Number of grid columns (1-5) + * + * @returns 201 with created bingo + * @returns 400 if validation fails + * @returns 404 if event does not exist + */ +export async function generateBingo(req: Request, res: Response) { + try { + const { _eventId, description, topic, n_rows, n_cols } = req.body; + + if (!_eventId) { + return res.status(400).json({ + success: false, + msg: "_eventId is required", + }); + } + + if (!Types.ObjectId.isValid(_eventId)) { + return res.status(400).json({ + success: false, + msg: "_eventId must be a valid ObjectId", + }); + } + + if (!description) { + return res.status(400).json({ + success: false, + msg: "description is required", + }); + } + + if (!topic) { + return res.status(400).json({ + success: false, + msg: "topic is required", + }); + } + + if ( + typeof n_rows !== "number" || + typeof n_cols !== "number" || + n_rows <= 0 || + n_cols <= 0 || + n_rows > 5 || + n_cols > 5 + ) { + return res.status(400).json({ + success: false, + msg: "n_rows and n_cols must be numbers where 0 < value <= 5", + }); + } + + const eventExists = await Event.findById(_eventId).select("_id"); + if (!eventExists) { + return res.status(404).json({ + success: false, + msg: "Event not found", + }); + } + + const aiResult = await generateBingoGrid( n_rows, n_cols, topic); + + let processed_ai_result: string[][] = process_ai_result(aiResult); + + + const bingo = await Bingo.create({ + _eventId, + description, + grid: processed_ai_result, + }); + + return res.status(201).json({ + success: true, + bingoId: bingo._id, + bingo, + }); + } catch (err: any) { + return res.status(500).json({ + success: false, + error: err.message, + }); + } } \ No newline at end of file From 689cb297fd64d501d6275f929753307cbe384fe9 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 12 Mar 2026 14:11:07 -0600 Subject: [PATCH 07/79] updated bingo controller logic to only generate and bingo.txt prompt --- shatter-backend/src/ai/prompts/bingo.txt | 5 +- .../src/controllers/bingo_controller.ts | 172 +++++++----------- 2 files changed, 69 insertions(+), 108 deletions(-) diff --git a/shatter-backend/src/ai/prompts/bingo.txt b/shatter-backend/src/ai/prompts/bingo.txt index a0941a6..17baefb 100644 --- a/shatter-backend/src/ai/prompts/bingo.txt +++ b/shatter-backend/src/ai/prompts/bingo.txt @@ -1,9 +1,10 @@ Generate creative bingo entries based on the provided event context. Content guidelines: -- Each entry should be short and fit naturally in one bingo square. +- Each entry should be short. - Make entries playful, workplace-relevant, and easy to recognize. -- Favor software team culture, meetings, product/design/engineering habits, and tech workplace jokes. +- Favor team culture, meetings, product/design/engineering habits, and workplace jokes. +- If making quotes, include `Has said:` before the generated quote. - Keep the tone lighthearted and suitable for a professional team event. - Avoid generic party prompts. - Avoid offensive, overly personal, or inappropriate content. diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index adfc57b..8310b27 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -248,70 +248,71 @@ function buildShapeExample(rows: number, cols: number): string { /** * Calls Gemini to generate a bingo grid */ -async function generateBingoGrid(n_rows: number, n_cols: number, topic: string): Promise> { +async function generateBingoGrid(n_rows: number, n_cols: number, context: string): Promise> { + const schema = buildSchema(n_rows, n_cols); - const schemaJson = zodToJsonSchema(schema); - const example = buildShapeExample(n_rows, n_cols); - - const basePrompt = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: - ${example} - Rules: - - Keys must be row1, row2, row3, etc. - - Each row must contain ${n_cols} strings. - - Each string is a humorous bingo phrase about software development culture. - Return ONLY valid JSON. - `.trim(); - const promptPath = "../ai/prompts/bingo.txt"; - - if (!fs.existsSync(promptPath)) { - throw new Error(`Prompt file missing: ${promptPath}`); - } - const aiInstruction = fs.readFileSync(promptPath, "utf-8"); - - const aiPrompt = new Prompt([basePrompt, aiInstruction]) - - aiPrompt.generatePrompt(); - - const prompt = aiPrompt.getPrompt(); - console.log("----------- Final Prompt -----------"); - console.log(prompt); - console.log("------------------------------------"); - - try { - - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: prompt, - config: { - responseMimeType: "application/json", - responseJsonSchema: schemaJson, - temperature: 0.7, - }, - }); + const schemaJson = zodToJsonSchema(schema); + const example = buildShapeExample(n_rows, n_cols); + + const basePrompt_structure = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: + ${example} + Rules: + - Keys must be row1, row2, row3, etc. + - Each row must contain ${n_cols} strings. + Return ONLY valid JSON. + + You will be provided with additional context to inspire the content of the bingo squares. Use that context to generate relevant bingo square phrases. + `.trim(); + const userContext = `Additional context for bingo content:\n${context}`; + + const promptPath = "../ai/prompts/bingo.txt"; + if (!fs.existsSync(promptPath)) { + throw new Error(`Prompt file missing: ${promptPath}`); + } + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); - console.log("RAW RESPONSE:"); - console.log(response.text); + const aiPrompt = new Prompt([basePrompt_structure, userContext, aiInstruction]) + aiPrompt.generatePrompt(); + const prompt = aiPrompt.getPrompt(); + console.log("----------- Final Prompt -----------"); + console.log(prompt); + console.log("------------------------------------"); - const parsed = JSON.parse(response.text); - const validated: Record = schema.parse(parsed); - return validated; + try { - } catch (error) { - console.error("Generation failed:", error); - return makeEmptyGrid(n_rows, n_cols) - } + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: schemaJson, + temperature: 0.7, + }, + }); + + console.log("RAW RESPONSE:"); + console.log(response.text); + + const parsed = JSON.parse(response.text); + const validated: Record = schema.parse(parsed); + return validated; + + } catch (error) { + console.error("Generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) + } } const apiKey = process.env.GEMINI_API_KEY; const ai = new GoogleGenAI({ apiKey }); - +//takes geminis dumbass output and turns it into a more usable format function process_ai_result(ai_result: Record) { - let grid: string[][] = []; + const grid: string[][] = []; const keys: string[] = Object.keys(ai_result); for(let i = 0; i < keys.length; i++) { - let val: string[] = ai_result[keys[i]]; + const val: string[] = ai_result[keys[i]]; grid.push(val) } @@ -321,47 +322,23 @@ function process_ai_result(ai_result: Record) { /** * POST /api/bingo/generate * - * Generate an AI bingo grid for an event. + * Generate an AI bingo grid. * - * @param req.body._eventId - Event ObjectId (required) - * @param req.body.description - Description string (required but not used) - * @param req.body.topic - Topic for bingo content (required) + * @param req.body.context - Context for bingo content (required) * @param req.body.n_rows - Number of grid rows (1-5) * @param req.body.n_cols - Number of grid columns (1-5) * - * @returns 201 with created bingo + * @returns 200 with generated bingo grid * @returns 400 if validation fails - * @returns 404 if event does not exist */ export async function generateBingo(req: Request, res: Response) { try { - const { _eventId, description, topic, n_rows, n_cols } = req.body; + const { context, n_rows, n_cols } = req.body; - if (!_eventId) { + if (!context) { return res.status(400).json({ - success: false, - msg: "_eventId is required", - }); - } - - if (!Types.ObjectId.isValid(_eventId)) { - return res.status(400).json({ - success: false, - msg: "_eventId must be a valid ObjectId", - }); - } - - if (!description) { - return res.status(400).json({ - success: false, - msg: "description is required", - }); - } - - if (!topic) { - return res.status(400).json({ - success: false, - msg: "topic is required", + status: false, + msg: "context is required", }); } @@ -374,38 +351,21 @@ export async function generateBingo(req: Request, res: Response) { n_cols > 5 ) { return res.status(400).json({ - success: false, + status: false, msg: "n_rows and n_cols must be numbers where 0 < value <= 5", }); } - const eventExists = await Event.findById(_eventId).select("_id"); - if (!eventExists) { - return res.status(404).json({ - success: false, - msg: "Event not found", - }); - } - - const aiResult = await generateBingoGrid( n_rows, n_cols, topic); - - let processed_ai_result: string[][] = process_ai_result(aiResult); - - - const bingo = await Bingo.create({ - _eventId, - description, - grid: processed_ai_result, - }); + const aiResult = await generateBingoGrid(n_rows, n_cols, context); + const bingo_grid: string[][] = process_ai_result(aiResult); - return res.status(201).json({ - success: true, - bingoId: bingo._id, - bingo, + return res.status(200).json({ + status: true, + bingo_grid, }); } catch (err: any) { return res.status(500).json({ - success: false, + status: false, error: err.message, }); } From 3278a1bd95dbaa83b5a5cf20700d8029d1d761df Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 12 Mar 2026 14:18:57 -0600 Subject: [PATCH 08/79] added route for bingo game generation --- shatter-backend/src/routes/bingo_routes.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index 7529310..654edf9 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -1,16 +1,19 @@ import { Router } from 'express'; -import { createBingo, getBingo, updateBingo} from '../controllers/bingo_controller'; +import { createBingo, getBingo, updateBingo, generateBingo} from '../controllers/bingo_controller'; import { authMiddleware } from '../middleware/auth_middleware'; const router = Router(); router.post('/createBingo', authMiddleware, createBingo); -// POST /api/bingo/getBingo - get bingo details +// GET /api/bingo/getBingo - get bingo details router.get('/getBingo/:eventId', getBingo); -// POST /api/bingo/updateBingo - update bingo details +// PUT /api/bingo/updateBingo - update bingo details router.put("/updateBingo", authMiddleware, updateBingo); +// POST /api/bingo/generateBingo - generate bingo using AI for an event +router.put("/generateBingo", generateBingo); + export default router; From ec6051d2206fe55b347f93abc5904b918e162572 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 12 Mar 2026 14:53:50 -0600 Subject: [PATCH 09/79] Added route that works --- shatter-backend/src/controllers/bingo_controller.ts | 5 +---- shatter-backend/src/routes/bingo_routes.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 8310b27..8d830ed 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -266,10 +266,7 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string `.trim(); const userContext = `Additional context for bingo content:\n${context}`; - const promptPath = "../ai/prompts/bingo.txt"; - if (!fs.existsSync(promptPath)) { - throw new Error(`Prompt file missing: ${promptPath}`); - } + const promptPath = path.resolve(__dirname, "../ai/prompts/bingo.txt"); const aiInstruction = fs.readFileSync(promptPath, "utf-8"); const aiPrompt = new Prompt([basePrompt_structure, userContext, aiInstruction]) diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index 654edf9..0f46e9f 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -13,7 +13,7 @@ router.get('/getBingo/:eventId', getBingo); router.put("/updateBingo", authMiddleware, updateBingo); // POST /api/bingo/generateBingo - generate bingo using AI for an event -router.put("/generateBingo", generateBingo); +router.post("/generateBingo", generateBingo); export default router; From fc9efaf7b83be807337e2f068d04840d9d2eccf6 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:56:34 -0600 Subject: [PATCH 10/79] Update gitignore for vercel mobile --- shatter-mobile/.gitignore | 3 ++- shatter-mobile/vercel.json | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 shatter-mobile/vercel.json diff --git a/shatter-mobile/.gitignore b/shatter-mobile/.gitignore index b04db4d..c4b4c18 100644 --- a/shatter-mobile/.gitignore +++ b/shatter-mobile/.gitignore @@ -42,4 +42,5 @@ app-example /ios /android -.env \ No newline at end of file +.env +.vercel diff --git a/shatter-mobile/vercel.json b/shatter-mobile/vercel.json new file mode 100644 index 0000000..a2afd48 --- /dev/null +++ b/shatter-mobile/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} \ No newline at end of file From 10672a06ce612bf82af56b0be7b394fdf214a485 Mon Sep 17 00:00:00 2001 From: jktieh Date: Thu, 12 Mar 2026 15:11:58 -0600 Subject: [PATCH 11/79] Add icon components and update DashboardPage with new icons --- shatter-web/src/components/Hero.tsx | 15 +---- shatter-web/src/components/Navbar.tsx | 31 +-------- shatter-web/src/components/icons/BarsIcon.tsx | 20 ++++++ .../src/components/icons/CalendarIcon.tsx | 20 ++++++ .../src/components/icons/ChevronDownIcon.tsx | 20 ++++++ .../components/icons/ClipboardCopyIcon.tsx | 20 ++++++ .../src/components/icons/ClipboardIcon.tsx | 20 ++++++ .../src/components/icons/ClockIcon.tsx | 20 ++++++ .../src/components/icons/GoogleIcon.tsx | 24 +++++++ .../icons/InformationCircleIcon.tsx | 20 ++++++ shatter-web/src/components/icons/KeyIcon.tsx | 20 ++++++ shatter-web/src/components/icons/PlusIcon.tsx | 20 ++++++ .../src/components/icons/SearchIcon.tsx | 20 ++++++ .../src/components/icons/UsersIcon.tsx | 20 ++++++ shatter-web/src/components/icons/XIcon.tsx | 20 ++++++ shatter-web/src/components/icons/index.ts | 13 ++++ shatter-web/src/components/icons/types.ts | 6 ++ shatter-web/src/pages/DashboardPage.tsx | 63 +++++++------------ shatter-web/src/pages/EventPage.tsx | 45 ++----------- shatter-web/src/pages/LoginPage.tsx | 8 +-- 20 files changed, 318 insertions(+), 127 deletions(-) create mode 100644 shatter-web/src/components/icons/BarsIcon.tsx create mode 100644 shatter-web/src/components/icons/CalendarIcon.tsx create mode 100644 shatter-web/src/components/icons/ChevronDownIcon.tsx create mode 100644 shatter-web/src/components/icons/ClipboardCopyIcon.tsx create mode 100644 shatter-web/src/components/icons/ClipboardIcon.tsx create mode 100644 shatter-web/src/components/icons/ClockIcon.tsx create mode 100644 shatter-web/src/components/icons/GoogleIcon.tsx create mode 100644 shatter-web/src/components/icons/InformationCircleIcon.tsx create mode 100644 shatter-web/src/components/icons/KeyIcon.tsx create mode 100644 shatter-web/src/components/icons/PlusIcon.tsx create mode 100644 shatter-web/src/components/icons/SearchIcon.tsx create mode 100644 shatter-web/src/components/icons/UsersIcon.tsx create mode 100644 shatter-web/src/components/icons/XIcon.tsx create mode 100644 shatter-web/src/components/icons/index.ts create mode 100644 shatter-web/src/components/icons/types.ts diff --git a/shatter-web/src/components/Hero.tsx b/shatter-web/src/components/Hero.tsx index e581684..cbb921c 100644 --- a/shatter-web/src/components/Hero.tsx +++ b/shatter-web/src/components/Hero.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import QRCard from './QRCard'; +import { ChevronDownIcon } from './icons'; interface HeroProps { qrPayload?: string; @@ -229,19 +230,7 @@ const Hero: React.FC = ({ qrPayload = "hello" }) => { {/* Scroll Indicator */}
- - - +
{/* Custom Animations */} diff --git a/shatter-web/src/components/Navbar.tsx b/shatter-web/src/components/Navbar.tsx index 5d6e5b3..38e634c 100644 --- a/shatter-web/src/components/Navbar.tsx +++ b/shatter-web/src/components/Navbar.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import logo from "../assets/ShatterLogo_White.png"; +import { BarsIcon, XIcon } from "./icons"; export default function Navbar() { const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -194,35 +195,9 @@ export default function Navbar() { aria-label="Toggle menu" > {isMenuOpen ? ( - /* Close Icon */ - - - + ) : ( - /* Hamburger Icon */ - - - + )} diff --git a/shatter-web/src/components/icons/BarsIcon.tsx b/shatter-web/src/components/icons/BarsIcon.tsx new file mode 100644 index 0000000..12787cc --- /dev/null +++ b/shatter-web/src/components/icons/BarsIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function BarsIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/CalendarIcon.tsx b/shatter-web/src/components/icons/CalendarIcon.tsx new file mode 100644 index 0000000..ed1b0f9 --- /dev/null +++ b/shatter-web/src/components/icons/CalendarIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function CalendarIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ChevronDownIcon.tsx b/shatter-web/src/components/icons/ChevronDownIcon.tsx new file mode 100644 index 0000000..53ef640 --- /dev/null +++ b/shatter-web/src/components/icons/ChevronDownIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ChevronDownIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClipboardCopyIcon.tsx b/shatter-web/src/components/icons/ClipboardCopyIcon.tsx new file mode 100644 index 0000000..9f625c0 --- /dev/null +++ b/shatter-web/src/components/icons/ClipboardCopyIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClipboardCopyIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClipboardIcon.tsx b/shatter-web/src/components/icons/ClipboardIcon.tsx new file mode 100644 index 0000000..cc94921 --- /dev/null +++ b/shatter-web/src/components/icons/ClipboardIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClipboardIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/ClockIcon.tsx b/shatter-web/src/components/icons/ClockIcon.tsx new file mode 100644 index 0000000..da14c3b --- /dev/null +++ b/shatter-web/src/components/icons/ClockIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function ClockIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/GoogleIcon.tsx b/shatter-web/src/components/icons/GoogleIcon.tsx new file mode 100644 index 0000000..1927800 --- /dev/null +++ b/shatter-web/src/components/icons/GoogleIcon.tsx @@ -0,0 +1,24 @@ +import type { IconProps } from "./types"; + +export function GoogleIcon({ className = "w-5 h-5", ...props }: IconProps) { + return ( + + + + + + + ); +} diff --git a/shatter-web/src/components/icons/InformationCircleIcon.tsx b/shatter-web/src/components/icons/InformationCircleIcon.tsx new file mode 100644 index 0000000..4d17882 --- /dev/null +++ b/shatter-web/src/components/icons/InformationCircleIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function InformationCircleIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/KeyIcon.tsx b/shatter-web/src/components/icons/KeyIcon.tsx new file mode 100644 index 0000000..da05a75 --- /dev/null +++ b/shatter-web/src/components/icons/KeyIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function KeyIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/PlusIcon.tsx b/shatter-web/src/components/icons/PlusIcon.tsx new file mode 100644 index 0000000..0ebfdeb --- /dev/null +++ b/shatter-web/src/components/icons/PlusIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function PlusIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/SearchIcon.tsx b/shatter-web/src/components/icons/SearchIcon.tsx new file mode 100644 index 0000000..c1a2cd6 --- /dev/null +++ b/shatter-web/src/components/icons/SearchIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function SearchIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/UsersIcon.tsx b/shatter-web/src/components/icons/UsersIcon.tsx new file mode 100644 index 0000000..f506fa5 --- /dev/null +++ b/shatter-web/src/components/icons/UsersIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function UsersIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/XIcon.tsx b/shatter-web/src/components/icons/XIcon.tsx new file mode 100644 index 0000000..05fe53b --- /dev/null +++ b/shatter-web/src/components/icons/XIcon.tsx @@ -0,0 +1,20 @@ +import type { IconProps } from "./types"; + +export function XIcon({ className = "w-4 h-4", ...props }: IconProps) { + return ( + + + + ); +} diff --git a/shatter-web/src/components/icons/index.ts b/shatter-web/src/components/icons/index.ts new file mode 100644 index 0000000..ffc696b --- /dev/null +++ b/shatter-web/src/components/icons/index.ts @@ -0,0 +1,13 @@ +export { BarsIcon } from "./BarsIcon"; +export { CalendarIcon } from "./CalendarIcon"; +export { ChevronDownIcon } from "./ChevronDownIcon"; +export { ClipboardCopyIcon } from "./ClipboardCopyIcon"; +export { ClipboardIcon } from "./ClipboardIcon"; +export { ClockIcon } from "./ClockIcon"; +export { GoogleIcon } from "./GoogleIcon"; +export { InformationCircleIcon } from "./InformationCircleIcon"; +export { KeyIcon } from "./KeyIcon"; +export { PlusIcon } from "./PlusIcon"; +export { SearchIcon } from "./SearchIcon"; +export { UsersIcon } from "./UsersIcon"; +export { XIcon } from "./XIcon"; diff --git a/shatter-web/src/components/icons/types.ts b/shatter-web/src/components/icons/types.ts new file mode 100644 index 0000000..a580585 --- /dev/null +++ b/shatter-web/src/components/icons/types.ts @@ -0,0 +1,6 @@ +import type { SVGProps } from "react"; + +export interface IconProps extends SVGProps { + className?: string; + size?: number; +} diff --git a/shatter-web/src/pages/DashboardPage.tsx b/shatter-web/src/pages/DashboardPage.tsx index 2aa0be0..71b99ae 100644 --- a/shatter-web/src/pages/DashboardPage.tsx +++ b/shatter-web/src/pages/DashboardPage.tsx @@ -2,6 +2,17 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import Navbar from "../components/Navbar"; import Footer from "../components/Footer"; +import { + CalendarIcon, + ClipboardIcon, + ClockIcon, + InformationCircleIcon, + KeyIcon, + PlusIcon, + SearchIcon, + UsersIcon, + XIcon, +} from "../components/icons"; interface Event { _id: string; @@ -383,9 +394,7 @@ function DashboardPage() { {!loading && events.length === 0 && (
- - - +

No Events Yet

You haven't created any events. Start by creating your first event!

@@ -415,9 +424,7 @@ function DashboardPage() { className="p-2 rounded-lg hover:bg-white/10 transition-colors" title="Create new event" > - - - +
@@ -447,9 +454,7 @@ function DashboardPage() {
- - - + {formatDate(event.startDate)}
@@ -606,9 +611,7 @@ function DashboardPage() {
- - - + Start Date

{formatDate(selectedEvent.startDate)}

@@ -617,9 +620,7 @@ function DashboardPage() {
- - - + End Date

{formatDate(selectedEvent.endDate)}

@@ -628,9 +629,7 @@ function DashboardPage() {
- - - + Participants

@@ -640,9 +639,7 @@ function DashboardPage() {

- - - + Join Code

{selectedEvent.joinCode}

@@ -662,9 +659,7 @@ function DashboardPage() { >
- - - +

Name Bingo

@@ -674,9 +669,7 @@ function DashboardPage() {
@@ -762,9 +749,7 @@ function DashboardPage() {
- - - +

Tips for creating good bingo questions: