@studious-lms/server 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/.dockerignore +28 -0
  2. package/.env.example +5 -0
  3. package/.github/workflows/deploy.yml +123 -0
  4. package/Dockerfile +54 -0
  5. package/dist/index.js +5 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/instrument.js +3 -3
  8. package/dist/instrument.js.map +1 -1
  9. package/dist/lib/config/cors.d.ts.map +1 -1
  10. package/dist/lib/config/cors.js +31 -11
  11. package/dist/lib/config/cors.js.map +1 -1
  12. package/dist/lib/config/env.d.ts +17 -0
  13. package/dist/lib/config/env.d.ts.map +1 -1
  14. package/dist/lib/config/env.js +8 -2
  15. package/dist/lib/config/env.js.map +1 -1
  16. package/dist/lib/googleCloudStorage.d.ts +10 -0
  17. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  18. package/dist/lib/googleCloudStorage.js +22 -2
  19. package/dist/lib/googleCloudStorage.js.map +1 -1
  20. package/dist/lib/inferDocumentFormat.d.ts +10 -0
  21. package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
  22. package/dist/lib/inferDocumentFormat.js +48 -0
  23. package/dist/lib/inferDocumentFormat.js.map +1 -0
  24. package/dist/lib/legal.d.ts +3 -0
  25. package/dist/lib/legal.d.ts.map +1 -0
  26. package/dist/lib/legal.js +6 -0
  27. package/dist/lib/legal.js.map +1 -0
  28. package/dist/lib/prisma.d.ts +104 -5
  29. package/dist/lib/prisma.d.ts.map +1 -1
  30. package/dist/lib/prisma.js +33 -2
  31. package/dist/lib/prisma.js.map +1 -1
  32. package/dist/models/agenda.d.ts +20 -20
  33. package/dist/models/announcement.d.ts +211 -101
  34. package/dist/models/announcement.d.ts.map +1 -1
  35. package/dist/models/assignment.d.ts +560 -362
  36. package/dist/models/assignment.d.ts.map +1 -1
  37. package/dist/models/attendance.d.ts +197 -71
  38. package/dist/models/attendance.d.ts.map +1 -1
  39. package/dist/models/auth.d.ts +356 -114
  40. package/dist/models/auth.d.ts.map +1 -1
  41. package/dist/models/auth.js +4 -2
  42. package/dist/models/auth.js.map +1 -1
  43. package/dist/models/class.d.ts +327 -201
  44. package/dist/models/class.d.ts.map +1 -1
  45. package/dist/models/class.js +2 -2
  46. package/dist/models/class.js.map +1 -1
  47. package/dist/models/cloudStorage.d.ts +146 -56
  48. package/dist/models/cloudStorage.d.ts.map +1 -1
  49. package/dist/models/comment.d.ts +262 -110
  50. package/dist/models/comment.d.ts.map +1 -1
  51. package/dist/models/conversation.d.ts +206 -80
  52. package/dist/models/conversation.d.ts.map +1 -1
  53. package/dist/models/department.d.ts +196 -70
  54. package/dist/models/department.d.ts.map +1 -1
  55. package/dist/models/event.d.ts +305 -141
  56. package/dist/models/event.d.ts.map +1 -1
  57. package/dist/models/file.d.ts +422 -272
  58. package/dist/models/file.d.ts.map +1 -1
  59. package/dist/models/folder.d.ts +433 -181
  60. package/dist/models/folder.d.ts.map +1 -1
  61. package/dist/models/labChat.d.ts +205 -79
  62. package/dist/models/labChat.d.ts.map +1 -1
  63. package/dist/models/marketing.d.ts +79 -38
  64. package/dist/models/marketing.d.ts.map +1 -1
  65. package/dist/models/marketing.js +12 -4
  66. package/dist/models/marketing.js.map +1 -1
  67. package/dist/models/message.d.ts +97 -43
  68. package/dist/models/message.d.ts.map +1 -1
  69. package/dist/models/newtonChat.d.ts +84 -30
  70. package/dist/models/newtonChat.d.ts.map +1 -1
  71. package/dist/models/notification.d.ts +57 -21
  72. package/dist/models/notification.d.ts.map +1 -1
  73. package/dist/models/school.d.ts +349 -133
  74. package/dist/models/school.d.ts.map +1 -1
  75. package/dist/models/section.d.ts +216 -72
  76. package/dist/models/section.d.ts.map +1 -1
  77. package/dist/models/tag.d.ts +156 -48
  78. package/dist/models/tag.d.ts.map +1 -1
  79. package/dist/models/user.d.ts +82 -28
  80. package/dist/models/user.d.ts.map +1 -1
  81. package/dist/models/worksheet.d.ts +1032 -423
  82. package/dist/models/worksheet.d.ts.map +1 -1
  83. package/dist/models/worksheet.js +8 -2
  84. package/dist/models/worksheet.js.map +1 -1
  85. package/dist/pipelines/aiLabChat.d.ts +7 -9
  86. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  87. package/dist/pipelines/aiLabChat.js +194 -125
  88. package/dist/pipelines/aiLabChat.js.map +1 -1
  89. package/dist/pipelines/aiLabChatContract.d.ts +233 -389
  90. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  91. package/dist/pipelines/aiLabChatContract.js +54 -70
  92. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  93. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  94. package/dist/pipelines/aiNewtonChat.js +41 -41
  95. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  96. package/dist/pipelines/gradeWorksheet.d.ts +8 -8
  97. package/dist/pipelines/labChatPrompt.d.ts +18 -11
  98. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  99. package/dist/pipelines/labChatPrompt.js +93 -75
  100. package/dist/pipelines/labChatPrompt.js.map +1 -1
  101. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  102. package/dist/routers/_app.d.ts +2601 -2151
  103. package/dist/routers/_app.d.ts.map +1 -1
  104. package/dist/routers/_app.js +4 -2
  105. package/dist/routers/_app.js.map +1 -1
  106. package/dist/routers/admin.d.ts +13 -13
  107. package/dist/routers/agenda.d.ts +18 -18
  108. package/dist/routers/announcement.d.ts +60 -60
  109. package/dist/routers/assignment.d.ts +393 -383
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/attendance.d.ts +10 -10
  112. package/dist/routers/auth.d.ts +4 -2
  113. package/dist/routers/auth.d.ts.map +1 -1
  114. package/dist/routers/auth.js +15 -2
  115. package/dist/routers/auth.js.map +1 -1
  116. package/dist/routers/class.d.ts +157 -153
  117. package/dist/routers/class.d.ts.map +1 -1
  118. package/dist/routers/comment.d.ts +18 -18
  119. package/dist/routers/conversation.d.ts +11 -11
  120. package/dist/routers/department.d.ts +34 -34
  121. package/dist/routers/discussion.d.ts +68 -56
  122. package/dist/routers/discussion.d.ts.map +1 -1
  123. package/dist/routers/discussion.js +6 -2
  124. package/dist/routers/discussion.js.map +1 -1
  125. package/dist/routers/embedding.d.ts +73 -0
  126. package/dist/routers/embedding.d.ts.map +1 -0
  127. package/dist/routers/embedding.js +151 -0
  128. package/dist/routers/embedding.js.map +1 -0
  129. package/dist/routers/event.d.ts +57 -55
  130. package/dist/routers/event.d.ts.map +1 -1
  131. package/dist/routers/file.d.ts +14 -14
  132. package/dist/routers/folder.d.ts +38 -38
  133. package/dist/routers/labChat.d.ts +16 -16
  134. package/dist/routers/marketing.d.ts +1 -14
  135. package/dist/routers/marketing.d.ts.map +1 -1
  136. package/dist/routers/message.d.ts +13 -13
  137. package/dist/routers/newtonChat.d.ts +4 -4
  138. package/dist/routers/notifications.d.ts +5 -5
  139. package/dist/routers/permissions.d.ts +14 -14
  140. package/dist/routers/school.d.ts +59 -55
  141. package/dist/routers/school.d.ts.map +1 -1
  142. package/dist/routers/search.d.ts +22 -1
  143. package/dist/routers/search.d.ts.map +1 -1
  144. package/dist/routers/search.js +9 -3
  145. package/dist/routers/search.js.map +1 -1
  146. package/dist/routers/section.d.ts +4 -4
  147. package/dist/routers/studentProgress.d.ts +1 -1
  148. package/dist/routers/tag.d.ts +7 -7
  149. package/dist/routers/user.d.ts +99 -10
  150. package/dist/routers/user.d.ts.map +1 -1
  151. package/dist/routers/user.js +19 -2
  152. package/dist/routers/user.js.map +1 -1
  153. package/dist/routers/worksheet.d.ts +93 -55
  154. package/dist/routers/worksheet.d.ts.map +1 -1
  155. package/dist/routers/worksheet.js +26 -2
  156. package/dist/routers/worksheet.js.map +1 -1
  157. package/dist/routes/aiStream.d.ts +2 -0
  158. package/dist/routes/aiStream.d.ts.map +1 -0
  159. package/dist/routes/aiStream.js +726 -0
  160. package/dist/routes/aiStream.js.map +1 -0
  161. package/dist/seedDatabase.d.ts +3 -1
  162. package/dist/seedDatabase.d.ts.map +1 -1
  163. package/dist/seedDatabase.js +6 -6
  164. package/dist/seedDatabase.js.map +1 -1
  165. package/dist/services/adminInstitution.d.ts +8 -8
  166. package/dist/services/adminRunJob.d.ts +4 -4
  167. package/dist/services/agenda.d.ts +18 -18
  168. package/dist/services/aiChatStream.d.ts +88 -0
  169. package/dist/services/aiChatStream.d.ts.map +1 -0
  170. package/dist/services/aiChatStream.js +254 -0
  171. package/dist/services/aiChatStream.js.map +1 -0
  172. package/dist/services/aiModifierTools.d.ts +16 -0
  173. package/dist/services/aiModifierTools.d.ts.map +1 -0
  174. package/dist/services/aiModifierTools.js +274 -0
  175. package/dist/services/aiModifierTools.js.map +1 -0
  176. package/dist/services/aiProposalTools.d.ts +32 -0
  177. package/dist/services/aiProposalTools.d.ts.map +1 -0
  178. package/dist/services/aiProposalTools.js +207 -0
  179. package/dist/services/aiProposalTools.js.map +1 -0
  180. package/dist/services/aiStream.d.ts +22 -0
  181. package/dist/services/aiStream.d.ts.map +1 -0
  182. package/dist/services/aiStream.js +142 -0
  183. package/dist/services/aiStream.js.map +1 -0
  184. package/dist/services/aiTools.d.ts +16 -0
  185. package/dist/services/aiTools.d.ts.map +1 -0
  186. package/dist/services/aiTools.js +271 -0
  187. package/dist/services/aiTools.js.map +1 -0
  188. package/dist/services/announcement.d.ts +42 -42
  189. package/dist/services/announcement.d.ts.map +1 -1
  190. package/dist/services/announcement.js +7 -2
  191. package/dist/services/announcement.js.map +1 -1
  192. package/dist/services/assignment.d.ts +355 -345
  193. package/dist/services/assignment.d.ts.map +1 -1
  194. package/dist/services/assignment.js +30 -9
  195. package/dist/services/assignment.js.map +1 -1
  196. package/dist/services/attendance.d.ts +10 -10
  197. package/dist/services/auth.d.ts +4 -2
  198. package/dist/services/auth.d.ts.map +1 -1
  199. package/dist/services/auth.js +13 -4
  200. package/dist/services/auth.js.map +1 -1
  201. package/dist/services/class.d.ts +146 -142
  202. package/dist/services/class.d.ts.map +1 -1
  203. package/dist/services/class.js +24 -4
  204. package/dist/services/class.js.map +1 -1
  205. package/dist/services/comment.d.ts +18 -18
  206. package/dist/services/conversation.d.ts +11 -11
  207. package/dist/services/department.d.ts +33 -33
  208. package/dist/services/discussion.d.ts +58 -58
  209. package/dist/services/discussion.d.ts.map +1 -1
  210. package/dist/services/discussion.js +15 -8
  211. package/dist/services/discussion.js.map +1 -1
  212. package/dist/services/discussionSummary.d.ts +21 -0
  213. package/dist/services/discussionSummary.d.ts.map +1 -0
  214. package/dist/services/discussionSummary.js +179 -0
  215. package/dist/services/discussionSummary.js.map +1 -0
  216. package/dist/services/embedding.d.ts +85 -0
  217. package/dist/services/embedding.d.ts.map +1 -0
  218. package/dist/services/embedding.js +372 -0
  219. package/dist/services/embedding.js.map +1 -0
  220. package/dist/services/event.d.ts +55 -53
  221. package/dist/services/event.d.ts.map +1 -1
  222. package/dist/services/file.d.ts +28 -28
  223. package/dist/services/folder.d.ts +36 -36
  224. package/dist/services/folder.d.ts.map +1 -1
  225. package/dist/services/folder.js +32 -2
  226. package/dist/services/folder.js.map +1 -1
  227. package/dist/services/labChat.d.ts +16 -16
  228. package/dist/services/marketing.d.ts +2 -15
  229. package/dist/services/marketing.d.ts.map +1 -1
  230. package/dist/services/marketing.js +5 -5
  231. package/dist/services/marketing.js.map +1 -1
  232. package/dist/services/message.d.ts +11 -11
  233. package/dist/services/message.d.ts.map +1 -1
  234. package/dist/services/message.js +2 -2
  235. package/dist/services/message.js.map +1 -1
  236. package/dist/services/newtonChat.d.ts +4 -4
  237. package/dist/services/notification.d.ts +5 -5
  238. package/dist/services/notification.d.ts.map +1 -1
  239. package/dist/services/openaiDocumentGen.d.ts +37 -0
  240. package/dist/services/openaiDocumentGen.d.ts.map +1 -0
  241. package/dist/services/openaiDocumentGen.js +262 -0
  242. package/dist/services/openaiDocumentGen.js.map +1 -0
  243. package/dist/services/permissions.d.ts +14 -14
  244. package/dist/services/school.d.ts +54 -50
  245. package/dist/services/school.d.ts.map +1 -1
  246. package/dist/services/search.d.ts +13 -1
  247. package/dist/services/search.d.ts.map +1 -1
  248. package/dist/services/search.js +145 -2
  249. package/dist/services/search.js.map +1 -1
  250. package/dist/services/section.d.ts +3 -3
  251. package/dist/services/tag.d.ts +4 -4
  252. package/dist/services/user.d.ts +4 -4
  253. package/dist/services/user.d.ts.map +1 -1
  254. package/dist/services/user.js +10 -15
  255. package/dist/services/user.js.map +1 -1
  256. package/dist/services/userPrivacy.d.ts +86 -0
  257. package/dist/services/userPrivacy.d.ts.map +1 -0
  258. package/dist/services/userPrivacy.js +184 -0
  259. package/dist/services/userPrivacy.js.map +1 -0
  260. package/dist/services/worksheet.d.ts +86 -57
  261. package/dist/services/worksheet.d.ts.map +1 -1
  262. package/dist/services/worksheet.js +57 -5
  263. package/dist/services/worksheet.js.map +1 -1
  264. package/dist/utils/email.d.ts.map +1 -1
  265. package/dist/utils/email.js +7 -3
  266. package/dist/utils/email.js.map +1 -1
  267. package/dist/utils/streamBroadcast.d.ts +11 -0
  268. package/dist/utils/streamBroadcast.d.ts.map +1 -0
  269. package/dist/utils/streamBroadcast.js +32 -0
  270. package/dist/utils/streamBroadcast.js.map +1 -0
  271. package/dist/workers/connection.d.ts +15 -0
  272. package/dist/workers/connection.d.ts.map +1 -1
  273. package/dist/workers/connection.js +20 -2
  274. package/dist/workers/connection.js.map +1 -1
  275. package/dist/workers/index.js +4 -2
  276. package/dist/workers/index.js.map +1 -1
  277. package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
  278. package/dist/workers/processors/createRunJobWorker.js +31 -8
  279. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  280. package/dist/workers/processors/embedContent.d.ts +8 -0
  281. package/dist/workers/processors/embedContent.d.ts.map +1 -0
  282. package/dist/workers/processors/embedContent.js +67 -0
  283. package/dist/workers/processors/embedContent.js.map +1 -0
  284. package/dist/workers/queues.d.ts +6 -0
  285. package/dist/workers/queues.d.ts.map +1 -1
  286. package/dist/workers/queues.js +5 -3
  287. package/dist/workers/queues.js.map +1 -1
  288. package/package.json +5 -1
  289. package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
  290. package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
  291. package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
  292. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
  293. package/prisma/schema.prisma +44 -7
  294. package/src/index.ts +4 -0
  295. package/src/instrument.ts +1 -1
  296. package/src/lib/config/cors.ts +32 -9
  297. package/src/lib/config/env.ts +6 -0
  298. package/src/lib/googleCloudStorage.ts +21 -0
  299. package/src/lib/inferDocumentFormat.ts +58 -0
  300. package/src/lib/legal.ts +2 -0
  301. package/src/lib/prisma.ts +40 -1
  302. package/src/models/auth.ts +4 -0
  303. package/src/models/class.ts +3 -2
  304. package/src/models/marketing.ts +10 -2
  305. package/src/models/worksheet.ts +5 -2
  306. package/src/pipelines/aiLabChat.ts +245 -146
  307. package/src/pipelines/aiLabChatContract.ts +59 -69
  308. package/src/pipelines/aiNewtonChat.ts +46 -43
  309. package/src/pipelines/labChatPrompt.ts +115 -90
  310. package/src/routers/_app.ts +2 -0
  311. package/src/routers/auth.ts +13 -0
  312. package/src/routers/discussion.ts +7 -0
  313. package/src/routers/embedding.ts +164 -0
  314. package/src/routers/search.ts +10 -1
  315. package/src/routers/user.ts +26 -0
  316. package/src/routers/worksheet.ts +27 -0
  317. package/src/routes/aiStream.ts +827 -0
  318. package/src/seedDatabase.ts +4 -4
  319. package/src/services/aiChatStream.ts +341 -0
  320. package/src/services/aiModifierTools.ts +341 -0
  321. package/src/services/aiProposalTools.ts +274 -0
  322. package/src/services/aiStream.ts +175 -0
  323. package/src/services/aiTools.ts +290 -0
  324. package/src/services/announcement.ts +7 -0
  325. package/src/services/assignment.ts +31 -7
  326. package/src/services/auth.ts +14 -2
  327. package/src/services/class.ts +27 -2
  328. package/src/services/discussion.ts +22 -8
  329. package/src/services/discussionSummary.ts +226 -0
  330. package/src/services/embedding.ts +446 -0
  331. package/src/services/folder.ts +31 -0
  332. package/src/services/marketing.ts +3 -3
  333. package/src/services/message.ts +2 -2
  334. package/src/services/openaiDocumentGen.ts +319 -0
  335. package/src/services/search.ts +163 -1
  336. package/src/services/user.ts +9 -17
  337. package/src/services/userPrivacy.ts +196 -0
  338. package/src/services/worksheet.ts +77 -3
  339. package/src/utils/email.ts +5 -1
  340. package/src/utils/streamBroadcast.ts +48 -0
  341. package/src/workers/connection.ts +19 -0
  342. package/src/workers/index.ts +2 -0
  343. package/src/workers/processors/createRunJobWorker.ts +32 -6
  344. package/src/workers/processors/embedContent.ts +86 -0
  345. package/src/workers/queues.ts +5 -1
  346. package/tests/helpers/registerInput.ts +20 -0
  347. package/tests/integration/globalSetup.ts +10 -18
  348. package/tests/integration/routers/auth.test.ts +13 -18
  349. package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
  350. package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
  351. package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
  352. package/tests/unit/services/aiChatStream.test.ts +92 -0
  353. package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
  354. package/prisma/migrations/20250807062924_init/migration.sql +0 -436
  355. package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
  356. package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
  357. package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
  358. package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
  359. package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
  360. package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
  361. package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
  362. package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
  363. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
  364. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
  365. package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
  366. package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
  367. package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
  368. package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
  369. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
  370. package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
  371. package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
  372. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
  373. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
  374. package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
  375. /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
@@ -0,0 +1,13 @@
1
+ -- CreateTable (implicit many-to-many join for ClassTag <-> Worksheet)
2
+ CREATE TABLE "_WorksheetToTag" (
3
+ "A" TEXT NOT NULL,
4
+ "B" TEXT NOT NULL,
5
+ CONSTRAINT "_WorksheetToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "DiscussionTag"("id") ON DELETE CASCADE ON UPDATE CASCADE,
6
+ CONSTRAINT "_WorksheetToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Worksheet"("id") ON DELETE CASCADE ON UPDATE CASCADE
7
+ );
8
+
9
+ -- CreateIndex
10
+ CREATE UNIQUE INDEX "_WorksheetToTag_AB_unique" ON "_WorksheetToTag"("A", "B");
11
+
12
+ -- CreateIndex
13
+ CREATE INDEX "_WorksheetToTag_B_index" ON "_WorksheetToTag"("B");
@@ -0,0 +1,4 @@
1
+ -- AlterEnum
2
+ ALTER TYPE "EmbeddingSourceType" ADD VALUE 'DISCUSSION';
3
+ ALTER TYPE "EmbeddingSourceType" ADD VALUE 'SUBMISSION';
4
+ ALTER TYPE "EmbeddingSourceType" ADD VALUE 'SYLLABUS';
@@ -0,0 +1,3 @@
1
+ -- AlterTable
2
+ ALTER TABLE "User" ADD COLUMN "termsAcceptedAt" TIMESTAMP(3),
3
+ ADD COLUMN "termsVersion" TEXT;
@@ -5,7 +5,8 @@
5
5
  // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6
6
 
7
7
  generator client {
8
- provider = "prisma-client-js"
8
+ provider = "prisma-client-js"
9
+ previewFeatures = ["postgresqlExtensions"]
9
10
  // output = "../generated/prisma"
10
11
  }
11
12
 
@@ -15,9 +16,10 @@ generator client {
15
16
  // }
16
17
 
17
18
  datasource db {
18
- provider = "postgresql"
19
- url = env("DATABASE_URL")
20
- directUrl = env("DIRECT_URL")
19
+ provider = "postgresql"
20
+ url = env("DATABASE_URL")
21
+ directUrl = env("DIRECT_URL")
22
+ extensions = [vector]
21
23
  }
22
24
 
23
25
  enum AssignmentType {
@@ -194,6 +196,9 @@ model User {
194
196
  profile UserProfile?
195
197
  verified Boolean @default(false)
196
198
 
199
+ termsAcceptedAt DateTime?
200
+ termsVersion String?
201
+
197
202
  teacherIn Class[] @relation("UserTeacherToClass")
198
203
  studentIn Class[] @relation("UserStudentToClass")
199
204
 
@@ -321,7 +326,8 @@ model Class {
321
326
  tags Tag[] @relation("ClassTags")
322
327
  permissions Permission[] @relation("ClassPermissions")
323
328
  discussions Discussion[]
324
- discussionTags DiscussionTag[]
329
+ classTags ClassTag[]
330
+ embeddings Embedding[]
325
331
 
326
332
  discussionsEnabled Boolean @default(true)
327
333
  studentsCanCreateDiscussions Boolean @default(true)
@@ -542,7 +548,7 @@ model Discussion {
542
548
  isLocked Boolean @default(false)
543
549
  allowReplies Boolean @default(true)
544
550
  attachments File[] @relation("DiscussionFiles")
545
- tags DiscussionTag[] @relation("DiscussionToTag")
551
+ tags ClassTag[] @relation("DiscussionToTag")
546
552
  replies DiscussionReply[]
547
553
  reactions Reaction[]
548
554
  reads DiscussionRead[]
@@ -575,15 +581,17 @@ model DiscussionReply {
575
581
  @@index([parentId])
576
582
  }
577
583
 
578
- model DiscussionTag {
584
+ model ClassTag {
579
585
  id String @id @default(uuid())
580
586
  class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
581
587
  classId String
582
588
  name String
583
589
  color String @default("#3B82F6")
584
590
  discussions Discussion[] @relation("DiscussionToTag")
591
+ worksheets Worksheet[] @relation("WorksheetToTag")
585
592
 
586
593
  @@unique([classId, name])
594
+ @@map("DiscussionTag")
587
595
  }
588
596
 
589
597
  model DiscussionRead {
@@ -861,6 +869,7 @@ model Worksheet {
861
869
  runJob RunJob? @relation("WorksheetRunJob", fields: [runJobId], references: [id], onDelete: SetNull)
862
870
 
863
871
  status JobStatus? @default(NOT_STARTED)
872
+ tags ClassTag[] @relation("WorksheetToTag")
864
873
 
865
874
  @@unique([classId, sourceUrl])
866
875
  }
@@ -962,6 +971,34 @@ enum RunJobType {
962
971
  AI_LAB_CHAT
963
972
  AI_NEWTON_CHAT
964
973
  STUDENT_PROGRESS_CHAT
974
+ EMBED_CONTENT
975
+ }
976
+
977
+ enum EmbeddingSourceType {
978
+ FILE
979
+ ASSIGNMENT
980
+ WORKSHEET
981
+ ANNOUNCEMENT
982
+ SECTION
983
+ DISCUSSION
984
+ SUBMISSION
985
+ SYLLABUS
986
+ }
987
+
988
+ model Embedding {
989
+ id String @id @default(uuid())
990
+ classId String
991
+ sourceType EmbeddingSourceType
992
+ sourceId String
993
+ content String
994
+ vector Unsupported("vector(1536)")
995
+ metadata Json? @default("{}")
996
+ createdAt DateTime @default(now())
997
+
998
+ class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
999
+
1000
+ @@index([classId])
1001
+ @@index([sourceType, sourceId])
965
1002
  }
966
1003
 
967
1004
  enum RunJobStatus {
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import { v4 as uuidv4 } from 'uuid';
27
27
 
28
28
  import "./instrument.js";
29
29
  import { openAIClient } from './utils/inference.js';
30
+ import { aiStreamRouter } from './routes/aiStream.js';
30
31
 
31
32
  const app = express();
32
33
 
@@ -313,6 +314,9 @@ io.engine.on('connection_error', (err: Error) => {
313
314
  // Setup socket handlers
314
315
  setupSocketHandlers(io);
315
316
 
317
+ // AI streaming routes
318
+ app.use('/api/ai', aiStreamRouter);
319
+
316
320
  // File serving endpoint for secure file access
317
321
  app.get('/api/files/:fileId', async (req, res) => {
318
322
  try {
package/src/instrument.ts CHANGED
@@ -8,7 +8,7 @@ if (env.NODE_ENV !== 'test') {
8
8
  environment: env.NODE_ENV || 'development',
9
9
  // Setting this option to true will send default PII data to Sentry.
10
10
  // For example, automatic IP address collection on events
11
- sendDefaultPii: true,
11
+ sendDefaultPii: false,
12
12
  // @todo: disable in test environment
13
13
  enabled: true, // Explicitly disable in test environment
14
14
  });
@@ -5,18 +5,24 @@ type CorsEnv = {
5
5
  CORS_ALLOWED_ORIGIN_PATTERNS?: string;
6
6
  };
7
7
 
8
- const productionOrigins = [
8
+ // First-party Studious domains. These are always allowed, regardless of
9
+ // NODE_ENV, so local/dev servers can still serve the real frontends.
10
+ const studiousOrigins = [
9
11
  'https://www.studious.sh',
10
12
  'https://studious.sh',
11
13
  'https://dev.studious.sh',
12
- // @todo: remove in actual prod
13
- 'http://localhost:3000',
14
- 'http://localhost:3001',
15
- 'http://127.0.0.1:3000',
16
- 'http://127.0.0.1:3001',
14
+ 'https://superuser.studious.sh',
15
+ 'https://*.studious.sh',
16
+ 'https://www.studious.ac',
17
+ 'https://studious.ac',
18
+ 'https://dev.studious.ac',
19
+ 'https://superuser.studious.ac',
20
+ 'https://*.studious.ac',
17
21
  ];
18
22
 
19
- const nonProductionOrigins = [
23
+ // Local development frontends.
24
+ // @todo: gate these behind a non-production check before real prod.
25
+ const localhostOrigins = [
20
26
  'http://localhost:3000',
21
27
  'http://localhost:3001',
22
28
  'http://127.0.0.1:3000',
@@ -40,6 +46,17 @@ const parseList = (value?: string) =>
40
46
 
41
47
  const isDefined = <T>(value: T | undefined | null): value is T => Boolean(value);
42
48
 
49
+ const hasWildcard = (value: string) => value.includes('*');
50
+
51
+ // Turn a wildcard origin like `https://*.studious.ac` into an anchored regex.
52
+ // `*` matches one or more subdomain labels (e.g. `app` or `a.b`).
53
+ const wildcardOriginToRegExp = (value: string) => {
54
+ const escaped = value
55
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
56
+ .replace(/\\\*/g, '[a-z0-9-]+(?:\\.[a-z0-9-]+)*');
57
+ return new RegExp(`^${escaped}$`, 'i');
58
+ };
59
+
43
60
  const toCorsOrigin = (value: string) => {
44
61
  try {
45
62
  return new URL(value).origin;
@@ -79,14 +96,20 @@ export const isValidCorsOriginPatternList = (patterns?: string) => {
79
96
  };
80
97
 
81
98
  export const createCorsOriginMatcher = (env: CorsEnv) => {
82
- const allowedOrigins = Array.from(new Set([
83
- ...(env.NODE_ENV === 'production' ? productionOrigins : nonProductionOrigins),
99
+ const configuredOrigins = Array.from(new Set([
100
+ ...studiousOrigins,
101
+ ...localhostOrigins,
84
102
  env.NEXT_PUBLIC_APP_URL ? toCorsOrigin(env.NEXT_PUBLIC_APP_URL) : undefined,
85
103
  ...parseCorsOrigins(env.CORS_ALLOWED_ORIGINS),
86
104
  ].filter(isDefined)));
87
105
 
106
+ // Wildcard entries (e.g. `https://*.studious.ac`) can't be matched by exact
107
+ // string comparison, so they're compiled into regex patterns instead.
108
+ const allowedOrigins = configuredOrigins.filter((origin) => !hasWildcard(origin));
109
+
88
110
  const allowedOriginPatterns = [
89
111
  vercelAppOriginPattern,
112
+ ...configuredOrigins.filter(hasWildcard).map(wildcardOriginToRegExp),
90
113
  ...parseCorsOriginPatterns(env.CORS_ALLOWED_ORIGIN_PATTERNS),
91
114
  ];
92
115
 
@@ -35,6 +35,12 @@ const baseSchema = z.object({
35
35
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
36
36
  PORT: z.string().transform(Number).default('3001'),
37
37
  DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
38
+ /**
39
+ * Optional BullMQ key prefix. Defaults to `bull:<NODE_ENV>` (see
40
+ * `getBullPrefix`). Set explicitly to isolate queues when multiple
41
+ * databases share one Redis instance (e.g. several devs on one Upstash).
42
+ */
43
+ BULL_PREFIX: z.string().optional(),
38
44
  });
39
45
 
40
46
  // Production/development schema with all required vars
@@ -98,4 +98,25 @@ export async function copyFile(sourcePath: string, destPath: string): Promise<vo
98
98
  message: 'Failed to copy file in storage',
99
99
  });
100
100
  }
101
+ }
102
+
103
+ /**
104
+ * Returns the public URL for a GCS object.
105
+ * The object must be publicly readable (via ACL or bucket-level IAM).
106
+ */
107
+ export function getPublicUrl(filePath: string): string {
108
+ return `https://storage.googleapis.com/${env.GOOGLE_CLOUD_BUCKET_NAME}/${filePath}`;
109
+ }
110
+
111
+ /**
112
+ * Best-effort attempt to make a GCS object publicly readable.
113
+ * Silently ignores errors (e.g. uniform bucket-level access enabled).
114
+ */
115
+ export async function makeFilePublic(filePath: string): Promise<void> {
116
+ try {
117
+ await bucket.file(filePath).makePublic();
118
+ } catch {
119
+ // Bucket may use uniform access or already have a public IAM policy – either way, the
120
+ // public URL will work if the bucket allows it; nothing to do here.
121
+ }
101
122
  }
@@ -0,0 +1,58 @@
1
+ import type { DocumentFormat } from '../services/openaiDocumentGen.js';
2
+ import { normalizeDocumentFormat } from '../services/openaiDocumentGen.js';
3
+
4
+ type FormatRule = { format: DocumentFormat; pattern: RegExp };
5
+
6
+ /** Ordered most-specific first. */
7
+ const FORMAT_RULES: FormatRule[] = [
8
+ { format: 'pptx', pattern: /\b(presentation|powerpoint|power point|slides?|slide deck|deck|pptx|\.pptx)\b/i },
9
+ { format: 'xlsx', pattern: /\b(spreadsheet|excel|workbook|xlsx|gradebook|grade book|\.xlsx)\b/i },
10
+ { format: 'csv', pattern: /\b(csv|comma[- ]separated|\.csv)\b/i },
11
+ { format: 'docx', pattern: /\b(word doc(ument)?|docx|editable handout|\.docx)\b/i },
12
+ { format: 'png', pattern: /\b(diagram|chart|graph|plot|figure|infographic|\.png)\b/i },
13
+ { format: 'md', pattern: /\b(markdown|readme|\.md)\b/i },
14
+ { format: 'html', pattern: /\b(html|web ?page|\.html)\b/i },
15
+ { format: 'txt', pattern: /\b(plain text|text file|\.txt)\b/i },
16
+ ];
17
+
18
+ function inferFromText(...parts: Array<string | null | undefined>): DocumentFormat | null {
19
+ const hay = parts.filter(Boolean).join('\n');
20
+ if (!hay.trim()) return null;
21
+
22
+ for (const { format, pattern } of FORMAT_RULES) {
23
+ if (pattern.test(hay)) return format;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ /**
29
+ * Resolve the output format for a generated document.
30
+ *
31
+ * The lab model often passes `format: "pdf"` even for presentations/spreadsheets.
32
+ * Keyword signals from the teacher request, title, and body take precedence over
33
+ * a generic pdf default; a non-pdf explicit format is always respected.
34
+ */
35
+ export function inferDocumentFormat(
36
+ title: string,
37
+ content: string,
38
+ explicit?: string | null,
39
+ userRequest?: string | null,
40
+ ): DocumentFormat {
41
+ const explicitNorm = explicit?.trim()
42
+ ? normalizeDocumentFormat(explicit)
43
+ : null;
44
+
45
+ const fromKeywords = inferFromText(userRequest, title, content);
46
+
47
+ // Non-pdf explicit choice wins (model or tool args).
48
+ if (explicitNorm && explicitNorm !== 'pdf') {
49
+ return explicitNorm;
50
+ }
51
+
52
+ // Override pdf when the request clearly asks for slides, spreadsheets, etc.
53
+ if (fromKeywords && fromKeywords !== 'pdf') {
54
+ return fromKeywords;
55
+ }
56
+
57
+ return explicitNorm ?? 'pdf';
58
+ }
@@ -0,0 +1,2 @@
1
+ /** Must match the "Last updated" month in the client privacy/terms copy. */
2
+ export const CURRENT_TERMS_VERSION = "2025-11";
package/src/lib/prisma.ts CHANGED
@@ -30,6 +30,19 @@ const getDatabaseUrl = () => {
30
30
  }
31
31
  };
32
32
 
33
+ /**
34
+ * Resolve a profile-picture value stored in the DB to a browser-loadable URL.
35
+ * - `null` / empty → `null`
36
+ * - already an HTTP URL (e.g. dicebear) → returned as-is
37
+ * - raw GCS object path → converted to a public GCS URL
38
+ */
39
+ function resolveProfilePictureUrl(raw: string | null): string | null {
40
+ if (!raw) return null;
41
+ if (raw.startsWith('http')) return raw;
42
+ const bucket = env.GOOGLE_CLOUD_BUCKET_NAME;
43
+ return `https://storage.googleapis.com/${bucket}/${raw}`;
44
+ }
45
+
33
46
  const prismaClientSingleton = () => {
34
47
  return new PrismaClient({
35
48
  datasources: {
@@ -37,6 +50,23 @@ const prismaClientSingleton = () => {
37
50
  url: getDatabaseUrl(),
38
51
  },
39
52
  },
53
+ }).$extends({
54
+ result: {
55
+ userProfile: {
56
+ profilePicture: {
57
+ needs: { profilePicture: true },
58
+ compute(profile) {
59
+ return resolveProfilePictureUrl(profile.profilePicture);
60
+ },
61
+ },
62
+ profilePictureThumbnail: {
63
+ needs: { profilePictureThumbnail: true },
64
+ compute(profile) {
65
+ return resolveProfilePictureUrl(profile.profilePictureThumbnail);
66
+ },
67
+ },
68
+ },
69
+ },
40
70
  });
41
71
  };
42
72
 
@@ -53,4 +83,13 @@ if (env.NODE_ENV !== 'production') {
53
83
 
54
84
  process.on('beforeExit', async () => {
55
85
  await prisma.$disconnect();
56
- });
86
+ });
87
+
88
+ /** The concrete (extended) Prisma client type used throughout the app. */
89
+ export type AppPrismaClient = typeof prisma;
90
+
91
+ /** Transaction client type compatible with the extended Prisma client. */
92
+ export type AppTransactionClient = Omit<
93
+ AppPrismaClient,
94
+ '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
95
+ >;
@@ -102,6 +102,8 @@ export function createUser(data: {
102
102
  email: string;
103
103
  password: string;
104
104
  verified?: boolean;
105
+ termsAcceptedAt?: Date;
106
+ termsVersion?: string;
105
107
  }) {
106
108
  return prisma.user.create({
107
109
  data: {
@@ -109,6 +111,8 @@ export function createUser(data: {
109
111
  email: data.email,
110
112
  password: data.password,
111
113
  verified: data.verified ?? false,
114
+ termsAcceptedAt: data.termsAcceptedAt,
115
+ termsVersion: data.termsVersion,
112
116
  profile: { create: {} },
113
117
  },
114
118
  select: {
@@ -5,7 +5,8 @@
5
5
  */
6
6
 
7
7
  import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, SessionType, Worksheet } from "@prisma/client";
8
- import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
8
+ import type { WorksheetQuestion } from "@prisma/client";
9
+ import type { AppPrismaClient, AppTransactionClient } from "../lib/prisma.js";
9
10
 
10
11
  import { prisma } from "../lib/prisma.js";
11
12
  import { v4 as uuidv4 } from 'uuid';
@@ -477,7 +478,7 @@ async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string,
477
478
  }
478
479
 
479
480
  async function recursivelyCreateFiles(
480
- tx: PrismaClient | Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">,
481
+ tx: AppPrismaClient | AppTransactionClient,
481
482
  folderId: string,
482
483
  targetFolderId: string | undefined,
483
484
  pathMap: Map<string, string>
@@ -27,10 +27,18 @@ export function createSchoolDevelopmentProgram(data: {
27
27
  });
28
28
  }
29
29
 
30
- /** @returns School development program by ID. */
31
- export function findSchoolDevelopmentProgramById(id: string) {
30
+ /** Public status lookup — omits contact PII (email, phone, address). */
31
+ export function findSchoolDevelopmentProgramStatusById(id: string) {
32
32
  return prisma.schoolDevelopementProgram.findUnique({
33
33
  where: { id },
34
+ select: {
35
+ id: true,
36
+ name: true,
37
+ city: true,
38
+ country: true,
39
+ status: true,
40
+ submittedAt: true,
41
+ },
34
42
  });
35
43
  }
36
44
 
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Worksheet model – worksheets, questions, submissions, student progress, grading.
3
3
  */
4
- import type { Prisma } from "@prisma/client";
5
4
  import { prisma } from "../lib/prisma.js";
5
+ import type { AppTransactionClient } from "../lib/prisma.js";
6
6
  import type { WorksheetQuestionType, JobStatus } from "@prisma/client";
7
7
  import {
8
8
  classMemberOrSchoolAdminWhere,
@@ -45,6 +45,7 @@ export function findWorksheetByIdForClassMember(id: string, userId: string) {
45
45
  include: {
46
46
  questions: { orderBy: { order: "asc" as const } },
47
47
  class: true,
48
+ tags: true,
48
49
  },
49
50
  });
50
51
  }
@@ -59,6 +60,7 @@ export function findWorksheetByIdForTeacher(id: string, userId: string) {
59
60
  include: {
60
61
  questions: { orderBy: { order: "asc" as const } },
61
62
  class: true,
63
+ tags: true,
62
64
  },
63
65
  });
64
66
  }
@@ -69,6 +71,7 @@ export function findWorksheetsByClassId(classId: string) {
69
71
  where: { classId },
70
72
  include: {
71
73
  questions: { select: { id: true } },
74
+ tags: true,
72
75
  },
73
76
  orderBy: { createdAt: "desc" },
74
77
  });
@@ -121,7 +124,7 @@ export function deleteWorksheet(id: string) {
121
124
  * Does not copy student responses or assignment links.
122
125
  */
123
126
  export async function duplicateWorksheetWithClient(
124
- tx: Prisma.TransactionClient,
127
+ tx: AppTransactionClient,
125
128
  sourceWorksheetId: string,
126
129
  targetClassId: string,
127
130
  name: string