@studious-lms/server 1.26.0 → 1.28.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 (222) hide show
  1. package/.env.example +3 -1
  2. package/.github/workflows/npm-publish.yml +1 -0
  3. package/AGENTS.md +84 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +31 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/instrument.js +9 -4
  8. package/dist/instrument.js.map +1 -1
  9. package/dist/lib/fileUpload.d.ts +15 -0
  10. package/dist/lib/fileUpload.d.ts.map +1 -1
  11. package/dist/lib/fileUpload.js +43 -2
  12. package/dist/lib/fileUpload.js.map +1 -1
  13. package/dist/models/cloudStorage.d.ts +11 -0
  14. package/dist/models/cloudStorage.d.ts.map +1 -1
  15. package/dist/models/cloudStorage.js +18 -2
  16. package/dist/models/cloudStorage.js.map +1 -1
  17. package/dist/models/conversation.d.ts +1 -0
  18. package/dist/models/conversation.d.ts.map +1 -1
  19. package/dist/models/labChat.d.ts +40 -0
  20. package/dist/models/labChat.d.ts.map +1 -1
  21. package/dist/models/labChat.js +32 -2
  22. package/dist/models/labChat.js.map +1 -1
  23. package/dist/models/message.d.ts +3 -0
  24. package/dist/models/message.d.ts.map +1 -1
  25. package/dist/pipelines/aiLabChat.d.ts +2 -0
  26. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  27. package/dist/pipelines/aiLabChat.js +60 -16
  28. package/dist/pipelines/aiLabChat.js.map +1 -1
  29. package/dist/pipelines/aiLabChatContract.d.ts +170 -2
  30. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
  31. package/dist/pipelines/aiLabChatContract.js +30 -2
  32. package/dist/pipelines/aiLabChatContract.js.map +1 -1
  33. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
  34. package/dist/pipelines/aiNewtonChat.js +30 -5
  35. package/dist/pipelines/aiNewtonChat.js.map +1 -1
  36. package/dist/pipelines/generateVisualization.d.ts +33 -0
  37. package/dist/pipelines/generateVisualization.d.ts.map +1 -0
  38. package/dist/pipelines/generateVisualization.js +79 -0
  39. package/dist/pipelines/generateVisualization.js.map +1 -0
  40. package/dist/pipelines/labChatPrompt.d.ts +1 -1
  41. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  42. package/dist/pipelines/labChatPrompt.js +13 -14
  43. package/dist/pipelines/labChatPrompt.js.map +1 -1
  44. package/dist/routers/_app.d.ts +1118 -70
  45. package/dist/routers/_app.d.ts.map +1 -1
  46. package/dist/routers/_app.js +4 -2
  47. package/dist/routers/_app.js.map +1 -1
  48. package/dist/routers/admin.d.ts +297 -1
  49. package/dist/routers/admin.d.ts.map +1 -1
  50. package/dist/routers/admin.js +43 -2
  51. package/dist/routers/admin.js.map +1 -1
  52. package/dist/routers/class.d.ts.map +1 -1
  53. package/dist/routers/class.js +16 -3
  54. package/dist/routers/class.js.map +1 -1
  55. package/dist/routers/conversation.d.ts +1 -0
  56. package/dist/routers/conversation.d.ts.map +1 -1
  57. package/dist/routers/labChat.d.ts +10 -0
  58. package/dist/routers/labChat.d.ts.map +1 -1
  59. package/dist/routers/labChat.js +6 -3
  60. package/dist/routers/labChat.js.map +1 -1
  61. package/dist/routers/message.d.ts +37 -4
  62. package/dist/routers/message.d.ts.map +1 -1
  63. package/dist/routers/message.js +24 -7
  64. package/dist/routers/message.js.map +1 -1
  65. package/dist/routers/module.d.ts +1 -1
  66. package/dist/routers/parent.d.ts +18 -0
  67. package/dist/routers/parent.d.ts.map +1 -1
  68. package/dist/routers/parent.js +10 -3
  69. package/dist/routers/parent.js.map +1 -1
  70. package/dist/routers/studentProgress.d.ts +44 -0
  71. package/dist/routers/studentProgress.d.ts.map +1 -1
  72. package/dist/routers/studentProgress.js +12 -3
  73. package/dist/routers/studentProgress.js.map +1 -1
  74. package/dist/routers/visualization.d.ts +124 -0
  75. package/dist/routers/visualization.d.ts.map +1 -0
  76. package/dist/routers/visualization.js +41 -0
  77. package/dist/routers/visualization.js.map +1 -0
  78. package/dist/routes/aiStream.js +32 -2
  79. package/dist/routes/aiStream.js.map +1 -1
  80. package/dist/seedDatabase.d.ts.map +1 -1
  81. package/dist/seedDatabase.js +107 -3
  82. package/dist/seedDatabase.js.map +1 -1
  83. package/dist/services/adminClass.d.ts +153 -0
  84. package/dist/services/adminClass.d.ts.map +1 -0
  85. package/dist/services/adminClass.js +196 -0
  86. package/dist/services/adminClass.js.map +1 -0
  87. package/dist/services/adminInstitution.d.ts +2 -0
  88. package/dist/services/adminInstitution.d.ts.map +1 -1
  89. package/dist/services/adminInstitution.js +11 -4
  90. package/dist/services/adminInstitution.js.map +1 -1
  91. package/dist/services/adminMetrics.d.ts +8 -0
  92. package/dist/services/adminMetrics.d.ts.map +1 -1
  93. package/dist/services/adminMetrics.js +30 -3
  94. package/dist/services/adminMetrics.js.map +1 -1
  95. package/dist/services/adminSpam.d.ts +76 -0
  96. package/dist/services/adminSpam.d.ts.map +1 -0
  97. package/dist/services/adminSpam.js +163 -0
  98. package/dist/services/adminSpam.js.map +1 -0
  99. package/dist/services/adminSql.d.ts +36 -0
  100. package/dist/services/adminSql.d.ts.map +1 -0
  101. package/dist/services/adminSql.js +172 -0
  102. package/dist/services/adminSql.js.map +1 -0
  103. package/dist/services/adminTelemetry.d.ts +56 -0
  104. package/dist/services/adminTelemetry.d.ts.map +1 -0
  105. package/dist/services/adminTelemetry.js +147 -0
  106. package/dist/services/adminTelemetry.js.map +1 -0
  107. package/dist/services/adminUser.d.ts +45 -1
  108. package/dist/services/adminUser.d.ts.map +1 -1
  109. package/dist/services/adminUser.js +53 -13
  110. package/dist/services/adminUser.js.map +1 -1
  111. package/dist/services/aiChatStream.d.ts +15 -0
  112. package/dist/services/aiChatStream.d.ts.map +1 -1
  113. package/dist/services/aiChatStream.js +144 -74
  114. package/dist/services/aiChatStream.js.map +1 -1
  115. package/dist/services/aiGeneration.d.ts +4 -0
  116. package/dist/services/aiGeneration.d.ts.map +1 -0
  117. package/dist/services/aiGeneration.js +88 -0
  118. package/dist/services/aiGeneration.js.map +1 -0
  119. package/dist/services/aiProposalTools.d.ts +4 -1
  120. package/dist/services/aiProposalTools.d.ts.map +1 -1
  121. package/dist/services/aiProposalTools.js +128 -3
  122. package/dist/services/aiProposalTools.js.map +1 -1
  123. package/dist/services/aiProposals.d.ts +70 -0
  124. package/dist/services/aiProposals.d.ts.map +1 -0
  125. package/dist/services/aiProposals.js +363 -0
  126. package/dist/services/aiProposals.js.map +1 -0
  127. package/dist/services/auth.d.ts.map +1 -1
  128. package/dist/services/auth.js +10 -4
  129. package/dist/services/auth.js.map +1 -1
  130. package/dist/services/conversation.d.ts +1 -0
  131. package/dist/services/conversation.d.ts.map +1 -1
  132. package/dist/services/fees.d.ts.map +1 -1
  133. package/dist/services/fees.js +7 -3
  134. package/dist/services/fees.js.map +1 -1
  135. package/dist/services/labChat.d.ts +10 -0
  136. package/dist/services/labChat.d.ts.map +1 -1
  137. package/dist/services/labChat.js +52 -3
  138. package/dist/services/labChat.js.map +1 -1
  139. package/dist/services/message.d.ts +24 -8
  140. package/dist/services/message.d.ts.map +1 -1
  141. package/dist/services/message.js +170 -117
  142. package/dist/services/message.js.map +1 -1
  143. package/dist/services/module.d.ts.map +1 -1
  144. package/dist/services/module.js +23 -4
  145. package/dist/services/module.js.map +1 -1
  146. package/dist/services/parentPortal.d.ts +14 -0
  147. package/dist/services/parentPortal.d.ts.map +1 -1
  148. package/dist/services/parentPortal.js +72 -2
  149. package/dist/services/parentPortal.js.map +1 -1
  150. package/dist/services/studentProgress.d.ts +50 -0
  151. package/dist/services/studentProgress.d.ts.map +1 -1
  152. package/dist/services/studentProgress.js +107 -2
  153. package/dist/services/studentProgress.js.map +1 -1
  154. package/dist/services/visualization.d.ts +72 -0
  155. package/dist/services/visualization.d.ts.map +1 -0
  156. package/dist/services/visualization.js +122 -0
  157. package/dist/services/visualization.js.map +1 -0
  158. package/dist/utils/inference.d.ts +10 -0
  159. package/dist/utils/inference.d.ts.map +1 -1
  160. package/dist/utils/inference.js +77 -2
  161. package/dist/utils/inference.js.map +1 -1
  162. package/dist/utils/sentryTrpc.d.ts +3 -0
  163. package/dist/utils/sentryTrpc.d.ts.map +1 -0
  164. package/dist/utils/sentryTrpc.js +26 -0
  165. package/dist/utils/sentryTrpc.js.map +1 -0
  166. package/dist/workers/processors/createRunJobWorker.js +17 -16
  167. package/dist/workers/processors/createRunJobWorker.js.map +1 -1
  168. package/package.json +1 -1
  169. package/prisma/migrations/20260811130000_add_ai_message_feedback/migration.sql +7 -0
  170. package/prisma/migrations/20261025120000_add_visualizations/migration.sql +27 -0
  171. package/prisma/schema.prisma +32 -0
  172. package/src/index.ts +30 -4
  173. package/src/instrument.ts +8 -2
  174. package/src/lib/fileUpload.ts +49 -0
  175. package/src/models/cloudStorage.ts +19 -0
  176. package/src/models/labChat.ts +34 -0
  177. package/src/pipelines/aiLabChat.ts +79 -18
  178. package/src/pipelines/aiLabChatContract.ts +35 -0
  179. package/src/pipelines/aiNewtonChat.ts +34 -2
  180. package/src/pipelines/generateVisualization.ts +107 -0
  181. package/src/pipelines/labChatPrompt.ts +11 -12
  182. package/src/routers/_app.ts +2 -0
  183. package/src/routers/admin.ts +72 -0
  184. package/src/routers/class.ts +14 -1
  185. package/src/routers/labChat.ts +5 -0
  186. package/src/routers/message.ts +34 -5
  187. package/src/routers/parent.ts +11 -0
  188. package/src/routers/studentProgress.ts +15 -0
  189. package/src/routers/visualization.ts +60 -0
  190. package/src/routes/aiStream.ts +32 -0
  191. package/src/seedDatabase.ts +107 -2
  192. package/src/services/adminClass.ts +222 -0
  193. package/src/services/adminInstitution.ts +9 -2
  194. package/src/services/adminMetrics.ts +35 -0
  195. package/src/services/adminSpam.ts +224 -0
  196. package/src/services/adminSql.ts +214 -0
  197. package/src/services/adminTelemetry.ts +194 -0
  198. package/src/services/adminUser.ts +66 -11
  199. package/src/services/aiChatStream.ts +83 -7
  200. package/src/services/aiGeneration.ts +92 -0
  201. package/src/services/aiProposalTools.ts +166 -1
  202. package/src/services/aiProposals.ts +428 -0
  203. package/src/services/auth.ts +8 -2
  204. package/src/services/fees.ts +5 -1
  205. package/src/services/labChat.ts +65 -0
  206. package/src/services/message.ts +205 -142
  207. package/src/services/module.ts +29 -2
  208. package/src/services/parentPortal.ts +85 -0
  209. package/src/services/studentProgress.ts +135 -0
  210. package/src/services/visualization.ts +147 -0
  211. package/src/utils/inference.ts +85 -0
  212. package/src/utils/sentryTrpc.ts +25 -0
  213. package/src/workers/processors/createRunJobWorker.ts +17 -17
  214. package/tests/unit/lib/fileUploadSession.test.ts +67 -0
  215. package/tests/unit/services/adminClass.test.ts +102 -0
  216. package/tests/unit/services/adminSpam.test.ts +108 -0
  217. package/tests/unit/services/adminSql.test.ts +56 -0
  218. package/tests/unit/services/adminTelemetry.test.ts +47 -0
  219. package/tests/unit/services/adminUser.test.ts +77 -0
  220. package/tests/unit/services/visualization.test.ts +130 -0
  221. package/tests/unit/utils/adminPanelAccess.test.ts +26 -0
  222. package/tests/unit/utils/sentryTrpc.test.ts +15 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * AI creation proposals – defer-and-confirm flow for Lab chat.
3
+ *
4
+ * The lab pipeline records every `propose_*` tool call as a pending creation
5
+ * on the AI message's meta (`meta.pendingCreations`). Nothing is created at
6
+ * generation time: the teacher approves or dismisses each proposal from the
7
+ * chat, and approval creates the real entity server-side.
8
+ */
9
+
10
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3505287f-3d05-5c3f-a2c7-fe3fe40b3803")}catch(e){}}();
11
+ import { TRPCError } from "@trpc/server";
12
+ import { v4 } from "uuid";
13
+ import { prisma } from "../lib/prisma.js";
14
+ import { chatChannel, pusher } from "../lib/pusher.js";
15
+ import { logger } from "../utils/logger.js";
16
+ import { findConversationMembership, findMessageByIdMinimal, messageFileSelect, } from "../models/message.js";
17
+ import { createAssignmentRecord, updateAssignmentRecord } from "./assignment.js";
18
+ import { createWorksheetRecord, addQuestionToWorksheet, updateWorksheetRecord, } from "./worksheet.js";
19
+ import { createSectionRecord, updateSectionRecord } from "./section.js";
20
+ const PROPOSAL_TOOL_KIND = {
21
+ propose_assignment: "assignment",
22
+ propose_worksheet: "worksheet",
23
+ propose_section: "section",
24
+ propose_update_assignment: "assignmentUpdate",
25
+ propose_update_worksheet: "worksheetUpdate",
26
+ propose_update_section: "sectionUpdate",
27
+ };
28
+ /**
29
+ * Turn the stream's accumulated proposals into pending creations and stamp
30
+ * each `propose_*` tool part with its proposal id so the client can render
31
+ * the confirmation card inline at the tool-call position.
32
+ */
33
+ export function buildPendingCreations(proposals, parts) {
34
+ const pendingCreations = [
35
+ ...proposals.assignments.map((payload) => ({
36
+ id: v4(), kind: "assignment", title: payload.title, payload, status: "pending",
37
+ })),
38
+ ...proposals.worksheets.map((payload) => ({
39
+ id: v4(), kind: "worksheet", title: payload.title, payload, status: "pending",
40
+ })),
41
+ ...proposals.sections.map((payload) => ({
42
+ id: v4(), kind: "section", title: payload.name, payload, status: "pending",
43
+ })),
44
+ ...proposals.assignmentUpdates.map((payload) => ({
45
+ id: v4(), kind: "assignmentUpdate", title: payload.targetTitle, payload, status: "pending",
46
+ })),
47
+ ...proposals.worksheetUpdates.map((payload) => ({
48
+ id: v4(), kind: "worksheetUpdate", title: payload.targetTitle, payload, status: "pending",
49
+ })),
50
+ ...proposals.sectionUpdates.map((payload) => ({
51
+ id: v4(), kind: "sectionUpdate", title: payload.targetTitle, payload, status: "pending",
52
+ })),
53
+ ];
54
+ const remaining = new Map();
55
+ for (const creation of pendingCreations) {
56
+ const list = remaining.get(creation.kind) ?? [];
57
+ list.push(creation);
58
+ remaining.set(creation.kind, list);
59
+ }
60
+ const stamped = parts.map((part) => {
61
+ if (part.type !== "tool")
62
+ return part;
63
+ const kind = PROPOSAL_TOOL_KIND[part.toolName];
64
+ if (!kind)
65
+ return part;
66
+ const list = remaining.get(kind);
67
+ if (!list || list.length === 0)
68
+ return part;
69
+ // Prefer a title match; fall back to stream order.
70
+ const idx = part.title
71
+ ? list.findIndex((c) => c.title.trim().toLowerCase() === part.title.trim().toLowerCase())
72
+ : 0;
73
+ const creation = list.splice(idx >= 0 ? idx : 0, 1)[0];
74
+ return { ...part, proposalId: creation.id };
75
+ });
76
+ return { pendingCreations, parts: stamped };
77
+ }
78
+ const RESOLVE_MAX_RETRIES = 5;
79
+ /**
80
+ * Compare-and-swap a proposal's status inside message meta. Returns the new
81
+ * meta on success, or null when the proposal was not in `fromStatus`.
82
+ */
83
+ async function casProposalStatus(messageId, proposalId, fromStatus, patch) {
84
+ for (let attempt = 0; attempt < RESOLVE_MAX_RETRIES; attempt++) {
85
+ const msg = await prisma.message.findUnique({
86
+ where: { id: messageId },
87
+ select: { meta: true },
88
+ });
89
+ if (!msg) {
90
+ throw new TRPCError({ code: "NOT_FOUND", message: "Message not found" });
91
+ }
92
+ const currentMeta = msg.meta ?? {};
93
+ const creations = Array.isArray(currentMeta.pendingCreations)
94
+ ? currentMeta.pendingCreations
95
+ : [];
96
+ const target = creations.find((c) => c.id === proposalId);
97
+ if (!target) {
98
+ throw new TRPCError({ code: "NOT_FOUND", message: "Proposal not found" });
99
+ }
100
+ if (target.status !== fromStatus)
101
+ return null;
102
+ const newMeta = {
103
+ ...currentMeta,
104
+ pendingCreations: creations.map((c) => c.id === proposalId ? { ...c, ...patch } : c),
105
+ };
106
+ const newMetaJson = JSON.stringify(newMeta);
107
+ const currentMetaJson = msg.meta === null ? null : JSON.stringify(msg.meta);
108
+ const updatedCount = currentMetaJson === null
109
+ ? await prisma.$executeRaw `
110
+ UPDATE "Message" SET meta = ${newMetaJson}::jsonb
111
+ WHERE id = ${messageId} AND meta IS NULL
112
+ `
113
+ : await prisma.$executeRaw `
114
+ UPDATE "Message" SET meta = ${newMetaJson}::jsonb
115
+ WHERE id = ${messageId} AND meta = ${currentMetaJson}::jsonb
116
+ `;
117
+ if (Number(updatedCount) > 0)
118
+ return newMeta;
119
+ }
120
+ throw new TRPCError({
121
+ code: "CONFLICT",
122
+ message: "Failed to update proposal after retries due to concurrent updates",
123
+ });
124
+ }
125
+ async function broadcastMessageMeta(messageId, conversationId) {
126
+ const updated = await prisma.message.findUnique({
127
+ where: { id: messageId },
128
+ include: {
129
+ sender: {
130
+ select: {
131
+ id: true,
132
+ username: true,
133
+ profile: { select: { displayName: true, profilePicture: true } },
134
+ },
135
+ },
136
+ attachments: { select: messageFileSelect },
137
+ mentions: { select: { userId: true } },
138
+ },
139
+ });
140
+ if (!updated)
141
+ return;
142
+ try {
143
+ await pusher.trigger(chatChannel(conversationId), "message-updated", {
144
+ id: updated.id,
145
+ content: updated.content,
146
+ senderId: updated.senderId,
147
+ conversationId: updated.conversationId,
148
+ status: updated.status,
149
+ createdAt: updated.createdAt,
150
+ sender: updated.sender,
151
+ attachments: updated.attachments ?? [],
152
+ meta: updated.meta,
153
+ mentionedUserIds: updated.mentions.map((m) => m.userId),
154
+ });
155
+ }
156
+ catch (error) {
157
+ logger.error("Failed to broadcast proposal resolution via Pusher", {
158
+ error,
159
+ messageId,
160
+ });
161
+ }
162
+ }
163
+ /** Create or update the proposed entity; returns the affected entity's id. */
164
+ async function createProposedEntity(userId, classId, proposal) {
165
+ switch (proposal.kind) {
166
+ case "sectionUpdate": {
167
+ const payload = proposal.payload;
168
+ await updateSectionRecord(userId, {
169
+ id: payload.id,
170
+ classId,
171
+ name: payload.name,
172
+ color: payload.color ?? undefined,
173
+ });
174
+ return payload.id;
175
+ }
176
+ case "worksheetUpdate": {
177
+ const payload = proposal.payload;
178
+ if (payload.name) {
179
+ await updateWorksheetRecord(payload.id, userId, { name: payload.name });
180
+ }
181
+ for (const question of payload.addQuestions ?? []) {
182
+ await addQuestionToWorksheet(payload.id, userId, {
183
+ question: question.question,
184
+ answer: question.answer,
185
+ points: question.points,
186
+ options: question.options,
187
+ markScheme: question.markScheme,
188
+ type: question.type,
189
+ order: question.order,
190
+ });
191
+ }
192
+ return payload.id;
193
+ }
194
+ case "assignmentUpdate": {
195
+ const payload = proposal.payload;
196
+ await updateAssignmentRecord(userId, {
197
+ id: payload.id,
198
+ classId,
199
+ title: payload.title,
200
+ instructions: payload.instructions,
201
+ dueDate: payload.dueDate,
202
+ type: payload.type,
203
+ maxGrade: payload.maxGrade,
204
+ acceptFiles: payload.acceptFiles,
205
+ acceptExtendedResponse: payload.acceptExtendedResponse,
206
+ acceptWorksheet: payload.acceptWorksheet,
207
+ sectionId: payload.sectionId,
208
+ });
209
+ return payload.id;
210
+ }
211
+ case "section": {
212
+ const section = await createSectionRecord(userId, {
213
+ classId,
214
+ name: proposal.payload.name,
215
+ color: proposal.payload.color ?? undefined,
216
+ });
217
+ return section.id;
218
+ }
219
+ case "worksheet": {
220
+ const worksheet = await createWorksheetRecord(classId, proposal.payload.title);
221
+ for (const question of proposal.payload.questions) {
222
+ await addQuestionToWorksheet(worksheet.id, userId, {
223
+ question: question.question,
224
+ answer: question.answer,
225
+ points: question.points,
226
+ options: question.options,
227
+ markScheme: question.markScheme,
228
+ type: question.type,
229
+ order: question.order,
230
+ });
231
+ }
232
+ return worksheet.id;
233
+ }
234
+ case "assignment": {
235
+ const payload = proposal.payload;
236
+ const assignment = await createAssignmentRecord(userId, {
237
+ classId,
238
+ title: payload.title,
239
+ instructions: payload.instructions,
240
+ dueDate: payload.dueDate,
241
+ type: payload.type,
242
+ maxGrade: payload.maxGrade,
243
+ acceptFiles: payload.acceptFiles,
244
+ acceptExtendedResponse: payload.acceptExtendedResponse,
245
+ acceptWorksheet: payload.acceptWorksheet,
246
+ gradingBoundaryId: payload.gradingBoundaryId ?? undefined,
247
+ markSchemeId: payload.markSchemeId ?? undefined,
248
+ worksheetIds: payload.worksheetIds,
249
+ studentIds: payload.studentIds,
250
+ sectionId: payload.sectionId ?? undefined,
251
+ existingFileIds: payload.attachments.map((a) => a.id),
252
+ });
253
+ return assignment.id;
254
+ }
255
+ }
256
+ }
257
+ /**
258
+ * Approve or dismiss a pending AI creation proposal. Approval creates the
259
+ * entity server-side; both outcomes persist to message meta and broadcast a
260
+ * `message-updated` event so every client (and reloads) see the final state.
261
+ */
262
+ export async function resolveAiProposal(userId, input) {
263
+ const { messageId, proposalId, action } = input;
264
+ const message = await findMessageByIdMinimal(messageId);
265
+ if (!message) {
266
+ throw new TRPCError({ code: "NOT_FOUND", message: "Message not found" });
267
+ }
268
+ const membership = await findConversationMembership(message.conversationId, userId);
269
+ if (!membership) {
270
+ throw new TRPCError({
271
+ code: "FORBIDDEN",
272
+ message: "Not a member of this conversation",
273
+ });
274
+ }
275
+ const labChat = await prisma.labChat.findFirst({
276
+ where: { conversationId: message.conversationId },
277
+ select: { classId: true },
278
+ });
279
+ if (!labChat) {
280
+ throw new TRPCError({
281
+ code: "BAD_REQUEST",
282
+ message: "Proposals can only be resolved in lab chats",
283
+ });
284
+ }
285
+ const teaches = await prisma.class.findFirst({
286
+ where: { id: labChat.classId, teachers: { some: { id: userId } } },
287
+ select: { id: true },
288
+ });
289
+ if (!teaches) {
290
+ throw new TRPCError({
291
+ code: "FORBIDDEN",
292
+ message: "Only class teachers can resolve proposals",
293
+ });
294
+ }
295
+ const currentMeta = await prisma.message.findUnique({
296
+ where: { id: messageId },
297
+ select: { meta: true },
298
+ });
299
+ const creations = Array.isArray(currentMeta?.meta?.pendingCreations)
300
+ ? currentMeta.meta
301
+ .pendingCreations
302
+ : [];
303
+ const proposal = creations.find((c) => c.id === proposalId);
304
+ if (!proposal) {
305
+ throw new TRPCError({ code: "NOT_FOUND", message: "Proposal not found" });
306
+ }
307
+ if (proposal.status !== "pending") {
308
+ return {
309
+ status: proposal.status,
310
+ createdId: proposal.createdId ?? null,
311
+ };
312
+ }
313
+ if (action === "dismiss") {
314
+ const meta = await casProposalStatus(messageId, proposalId, "pending", {
315
+ status: "dismissed",
316
+ });
317
+ if (meta) {
318
+ await broadcastMessageMeta(messageId, message.conversationId);
319
+ }
320
+ return { status: "dismissed", createdId: null };
321
+ }
322
+ // Approve: claim the proposal first so concurrent approvals create at most
323
+ // one entity, then create and record the created id (roll back on failure).
324
+ const claimed = await casProposalStatus(messageId, proposalId, "pending", {
325
+ status: "approved",
326
+ });
327
+ if (!claimed) {
328
+ const msg = await prisma.message.findUnique({
329
+ where: { id: messageId },
330
+ select: { meta: true },
331
+ });
332
+ const latest = (msg?.meta?.pendingCreations ??
333
+ []).find((c) => c.id === proposalId);
334
+ return {
335
+ status: latest?.status ?? "pending",
336
+ createdId: latest?.createdId ?? null,
337
+ };
338
+ }
339
+ try {
340
+ const createdId = await createProposedEntity(userId, labChat.classId, proposal);
341
+ await casProposalStatus(messageId, proposalId, "approved", { createdId });
342
+ await broadcastMessageMeta(messageId, message.conversationId);
343
+ return { status: "approved", createdId };
344
+ }
345
+ catch (error) {
346
+ logger.error("Failed to create approved AI proposal", {
347
+ error,
348
+ messageId,
349
+ proposalId,
350
+ kind: proposal.kind,
351
+ });
352
+ await casProposalStatus(messageId, proposalId, "approved", {
353
+ status: "pending",
354
+ }).catch(() => null);
355
+ await broadcastMessageMeta(messageId, message.conversationId);
356
+ throw new TRPCError({
357
+ code: "INTERNAL_SERVER_ERROR",
358
+ message: "Failed to create the approved item",
359
+ });
360
+ }
361
+ }
362
+ //# sourceMappingURL=aiProposals.js.map
363
+ //# debugId=3505287f-3d05-5c3f-a2c7-fe3fe40b3803
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aiProposals.js","sources":["services/aiProposals.ts"],"sourceRoot":"/","sourcesContent":["/**\n * AI creation proposals – defer-and-confirm flow for Lab chat.\n *\n * The lab pipeline records every `propose_*` tool call as a pending creation\n * on the AI message's meta (`meta.pendingCreations`). Nothing is created at\n * generation time: the teacher approves or dismisses each proposal from the\n * chat, and approval creates the real entity server-side.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { v4 } from \"uuid\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { chatChannel, pusher } from \"../lib/pusher.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findConversationMembership,\n findMessageByIdMinimal,\n messageFileSelect,\n} from \"../models/message.js\";\nimport { createAssignmentRecord, updateAssignmentRecord } from \"./assignment.js\";\nimport {\n createWorksheetRecord,\n addQuestionToWorksheet,\n updateWorksheetRecord,\n} from \"./worksheet.js\";\nimport { createSectionRecord, updateSectionRecord } from \"./section.js\";\nimport type { StreamPart } from \"./aiChatStream.js\";\nimport type { LabProposals } from \"./aiProposalTools.js\";\nimport type {\n AssignmentToCreate,\n WorksheetToCreate,\n SectionToCreate,\n AssignmentUpdate,\n WorksheetUpdate,\n SectionUpdate,\n} from \"../pipelines/aiLabChatContract.js\";\n\nexport type PendingCreationStatus = \"pending\" | \"approved\" | \"dismissed\";\n\nexport type PendingCreation =\n | { id: string; kind: \"assignment\"; title: string; payload: AssignmentToCreate; status: PendingCreationStatus; createdId?: string }\n | { id: string; kind: \"worksheet\"; title: string; payload: WorksheetToCreate; status: PendingCreationStatus; createdId?: string }\n | { id: string; kind: \"section\"; title: string; payload: SectionToCreate; status: PendingCreationStatus; createdId?: string }\n | { id: string; kind: \"assignmentUpdate\"; title: string; payload: AssignmentUpdate; status: PendingCreationStatus; createdId?: string }\n | { id: string; kind: \"worksheetUpdate\"; title: string; payload: WorksheetUpdate; status: PendingCreationStatus; createdId?: string }\n | { id: string; kind: \"sectionUpdate\"; title: string; payload: SectionUpdate; status: PendingCreationStatus; createdId?: string };\n\nconst PROPOSAL_TOOL_KIND: Record<string, PendingCreation[\"kind\"]> = {\n propose_assignment: \"assignment\",\n propose_worksheet: \"worksheet\",\n propose_section: \"section\",\n propose_update_assignment: \"assignmentUpdate\",\n propose_update_worksheet: \"worksheetUpdate\",\n propose_update_section: \"sectionUpdate\",\n};\n\n/**\n * Turn the stream's accumulated proposals into pending creations and stamp\n * each `propose_*` tool part with its proposal id so the client can render\n * the confirmation card inline at the tool-call position.\n */\nexport function buildPendingCreations(\n proposals: LabProposals,\n parts: StreamPart[],\n): { pendingCreations: PendingCreation[]; parts: StreamPart[] } {\n const pendingCreations: PendingCreation[] = [\n ...proposals.assignments.map((payload): PendingCreation => ({\n id: v4(), kind: \"assignment\", title: payload.title, payload, status: \"pending\",\n })),\n ...proposals.worksheets.map((payload): PendingCreation => ({\n id: v4(), kind: \"worksheet\", title: payload.title, payload, status: \"pending\",\n })),\n ...proposals.sections.map((payload): PendingCreation => ({\n id: v4(), kind: \"section\", title: payload.name, payload, status: \"pending\",\n })),\n ...proposals.assignmentUpdates.map((payload): PendingCreation => ({\n id: v4(), kind: \"assignmentUpdate\", title: payload.targetTitle, payload, status: \"pending\",\n })),\n ...proposals.worksheetUpdates.map((payload): PendingCreation => ({\n id: v4(), kind: \"worksheetUpdate\", title: payload.targetTitle, payload, status: \"pending\",\n })),\n ...proposals.sectionUpdates.map((payload): PendingCreation => ({\n id: v4(), kind: \"sectionUpdate\", title: payload.targetTitle, payload, status: \"pending\",\n })),\n ];\n\n const remaining = new Map<PendingCreation[\"kind\"], PendingCreation[]>();\n for (const creation of pendingCreations) {\n const list = remaining.get(creation.kind) ?? [];\n list.push(creation);\n remaining.set(creation.kind, list);\n }\n\n const stamped = parts.map((part) => {\n if (part.type !== \"tool\") return part;\n const kind = PROPOSAL_TOOL_KIND[part.toolName];\n if (!kind) return part;\n const list = remaining.get(kind);\n if (!list || list.length === 0) return part;\n // Prefer a title match; fall back to stream order.\n const idx = part.title\n ? list.findIndex((c) => c.title.trim().toLowerCase() === part.title!.trim().toLowerCase())\n : 0;\n const creation = list.splice(idx >= 0 ? idx : 0, 1)[0];\n return { ...part, proposalId: creation.id };\n });\n\n return { pendingCreations, parts: stamped };\n}\n\nconst RESOLVE_MAX_RETRIES = 5;\n\n/**\n * Compare-and-swap a proposal's status inside message meta. Returns the new\n * meta on success, or null when the proposal was not in `fromStatus`.\n */\nasync function casProposalStatus(\n messageId: string,\n proposalId: string,\n fromStatus: PendingCreationStatus,\n patch: Partial<Pick<PendingCreation, \"status\" | \"createdId\">>,\n): Promise<Record<string, unknown> | null> {\n for (let attempt = 0; attempt < RESOLVE_MAX_RETRIES; attempt++) {\n const msg = await prisma.message.findUnique({\n where: { id: messageId },\n select: { meta: true },\n });\n if (!msg) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Message not found\" });\n }\n\n const currentMeta = (msg.meta as Record<string, unknown>) ?? {};\n const creations = Array.isArray(currentMeta.pendingCreations)\n ? (currentMeta.pendingCreations as PendingCreation[])\n : [];\n const target = creations.find((c) => c.id === proposalId);\n if (!target) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Proposal not found\" });\n }\n if (target.status !== fromStatus) return null;\n\n const newMeta = {\n ...currentMeta,\n pendingCreations: creations.map((c) =>\n c.id === proposalId ? { ...c, ...patch } : c,\n ),\n };\n\n const newMetaJson = JSON.stringify(newMeta);\n const currentMetaJson = msg.meta === null ? null : JSON.stringify(msg.meta);\n const updatedCount =\n currentMetaJson === null\n ? await prisma.$executeRaw`\n UPDATE \"Message\" SET meta = ${newMetaJson}::jsonb\n WHERE id = ${messageId} AND meta IS NULL\n `\n : await prisma.$executeRaw`\n UPDATE \"Message\" SET meta = ${newMetaJson}::jsonb\n WHERE id = ${messageId} AND meta = ${currentMetaJson}::jsonb\n `;\n\n if (Number(updatedCount) > 0) return newMeta;\n }\n\n throw new TRPCError({\n code: \"CONFLICT\",\n message: \"Failed to update proposal after retries due to concurrent updates\",\n });\n}\n\nasync function broadcastMessageMeta(messageId: string, conversationId: string) {\n const updated = await prisma.message.findUnique({\n where: { id: messageId },\n include: {\n sender: {\n select: {\n id: true,\n username: true,\n profile: { select: { displayName: true, profilePicture: true } },\n },\n },\n attachments: { select: messageFileSelect },\n mentions: { select: { userId: true } },\n },\n });\n if (!updated) return;\n\n try {\n await pusher.trigger(chatChannel(conversationId), \"message-updated\", {\n id: updated.id,\n content: updated.content,\n senderId: updated.senderId,\n conversationId: updated.conversationId,\n status: updated.status,\n createdAt: updated.createdAt,\n sender: updated.sender,\n attachments: updated.attachments ?? [],\n meta: updated.meta,\n mentionedUserIds: updated.mentions.map((m) => m.userId),\n });\n } catch (error) {\n logger.error(\"Failed to broadcast proposal resolution via Pusher\", {\n error,\n messageId,\n });\n }\n}\n\n/** Create or update the proposed entity; returns the affected entity's id. */\nasync function createProposedEntity(\n userId: string,\n classId: string,\n proposal: PendingCreation,\n): Promise<string> {\n switch (proposal.kind) {\n case \"sectionUpdate\": {\n const payload = proposal.payload;\n await updateSectionRecord(userId, {\n id: payload.id,\n classId,\n name: payload.name,\n color: payload.color ?? undefined,\n });\n return payload.id;\n }\n case \"worksheetUpdate\": {\n const payload = proposal.payload;\n if (payload.name) {\n await updateWorksheetRecord(payload.id, userId, { name: payload.name });\n }\n for (const question of payload.addQuestions ?? []) {\n await addQuestionToWorksheet(payload.id, userId, {\n question: question.question,\n answer: question.answer,\n points: question.points,\n options: question.options,\n markScheme: question.markScheme,\n type: question.type,\n order: question.order,\n });\n }\n return payload.id;\n }\n case \"assignmentUpdate\": {\n const payload = proposal.payload;\n await updateAssignmentRecord(userId, {\n id: payload.id,\n classId,\n title: payload.title,\n instructions: payload.instructions,\n dueDate: payload.dueDate,\n type: payload.type,\n maxGrade: payload.maxGrade,\n acceptFiles: payload.acceptFiles,\n acceptExtendedResponse: payload.acceptExtendedResponse,\n acceptWorksheet: payload.acceptWorksheet,\n sectionId: payload.sectionId,\n });\n return payload.id;\n }\n case \"section\": {\n const section = await createSectionRecord(userId, {\n classId,\n name: proposal.payload.name,\n color: proposal.payload.color ?? undefined,\n });\n return section.id;\n }\n case \"worksheet\": {\n const worksheet = await createWorksheetRecord(classId, proposal.payload.title);\n for (const question of proposal.payload.questions) {\n await addQuestionToWorksheet(worksheet.id, userId, {\n question: question.question,\n answer: question.answer,\n points: question.points,\n options: question.options,\n markScheme: question.markScheme,\n type: question.type,\n order: question.order,\n });\n }\n return worksheet.id;\n }\n case \"assignment\": {\n const payload = proposal.payload;\n const assignment = await createAssignmentRecord(userId, {\n classId,\n title: payload.title,\n instructions: payload.instructions,\n dueDate: payload.dueDate,\n type: payload.type,\n maxGrade: payload.maxGrade,\n acceptFiles: payload.acceptFiles,\n acceptExtendedResponse: payload.acceptExtendedResponse,\n acceptWorksheet: payload.acceptWorksheet,\n gradingBoundaryId: payload.gradingBoundaryId ?? undefined,\n markSchemeId: payload.markSchemeId ?? undefined,\n worksheetIds: payload.worksheetIds,\n studentIds: payload.studentIds,\n sectionId: payload.sectionId ?? undefined,\n existingFileIds: payload.attachments.map((a) => a.id),\n });\n return assignment.id;\n }\n }\n}\n\n/**\n * Approve or dismiss a pending AI creation proposal. Approval creates the\n * entity server-side; both outcomes persist to message meta and broadcast a\n * `message-updated` event so every client (and reloads) see the final state.\n */\nexport async function resolveAiProposal(\n userId: string,\n input: { messageId: string; proposalId: string; action: \"approve\" | \"dismiss\" },\n) {\n const { messageId, proposalId, action } = input;\n\n const message = await findMessageByIdMinimal(messageId);\n if (!message) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Message not found\" });\n }\n\n const membership = await findConversationMembership(\n message.conversationId,\n userId,\n );\n if (!membership) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Not a member of this conversation\",\n });\n }\n\n const labChat = await prisma.labChat.findFirst({\n where: { conversationId: message.conversationId },\n select: { classId: true },\n });\n if (!labChat) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Proposals can only be resolved in lab chats\",\n });\n }\n\n const teaches = await prisma.class.findFirst({\n where: { id: labChat.classId, teachers: { some: { id: userId } } },\n select: { id: true },\n });\n if (!teaches) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can resolve proposals\",\n });\n }\n\n const currentMeta = await prisma.message.findUnique({\n where: { id: messageId },\n select: { meta: true },\n });\n const creations = Array.isArray(\n (currentMeta?.meta as Record<string, unknown> | null)?.pendingCreations,\n )\n ? ((currentMeta!.meta as Record<string, unknown>)\n .pendingCreations as PendingCreation[])\n : [];\n const proposal = creations.find((c) => c.id === proposalId);\n if (!proposal) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Proposal not found\" });\n }\n if (proposal.status !== \"pending\") {\n return {\n status: proposal.status,\n createdId: proposal.createdId ?? null,\n };\n }\n\n if (action === \"dismiss\") {\n const meta = await casProposalStatus(messageId, proposalId, \"pending\", {\n status: \"dismissed\",\n });\n if (meta) {\n await broadcastMessageMeta(messageId, message.conversationId);\n }\n return { status: \"dismissed\" as const, createdId: null };\n }\n\n // Approve: claim the proposal first so concurrent approvals create at most\n // one entity, then create and record the created id (roll back on failure).\n const claimed = await casProposalStatus(messageId, proposalId, \"pending\", {\n status: \"approved\",\n });\n if (!claimed) {\n const msg = await prisma.message.findUnique({\n where: { id: messageId },\n select: { meta: true },\n });\n const latest = (\n ((msg?.meta as Record<string, unknown> | null)?.pendingCreations ??\n []) as PendingCreation[]\n ).find((c) => c.id === proposalId);\n return {\n status: latest?.status ?? \"pending\",\n createdId: latest?.createdId ?? null,\n };\n }\n\n try {\n const createdId = await createProposedEntity(userId, labChat.classId, proposal);\n await casProposalStatus(messageId, proposalId, \"approved\", { createdId });\n await broadcastMessageMeta(messageId, message.conversationId);\n return { status: \"approved\" as const, createdId };\n } catch (error) {\n logger.error(\"Failed to create approved AI proposal\", {\n error,\n messageId,\n proposalId,\n kind: proposal.kind,\n });\n await casProposalStatus(messageId, proposalId, \"approved\", {\n status: \"pending\",\n }).catch(() => null);\n await broadcastMessageMeta(messageId, message.conversationId);\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to create the approved item\",\n });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"3505287f-3d05-5c3f-a2c7-fe3fe40b3803"}
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"/","sources":["services/auth.ts"],"names":[],"mappings":"AA8BA,qFAAqF;AACrF,wBAAsB,QAAQ,CAAC,KAAK,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;;;;;GA8EA;AAED,8DAA8D;AAC9D,wBAAsB,KAAK,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;GAoCxE;AAED,iEAAiE;AACjE,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM;;GAG1C;AAED,0EAA0E;AAC1E,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;GASzC;AAED,gFAAgF;AAChF,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM;;GAiC1D;AAED,8EAA8E;AAC9E,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM;;GA+BzC;AAED,kFAAkF;AAClF,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM;;GA+BvD;AAED,mFAAmF;AACnF,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;GAuBlE"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"/","sources":["services/auth.ts"],"names":[],"mappings":"AA8BA,qFAAqF;AACrF,wBAAsB,QAAQ,CAAC,KAAK,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,IAAI,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;;;;;GA8EA;AAED,8DAA8D;AAC9D,wBAAsB,KAAK,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;GAoCxE;AAED,iEAAiE;AACjE,wBAAsB,MAAM,CAAC,MAAM,EAAE,MAAM;;GAG1C;AAED,0EAA0E;AAC1E,wBAAsB,KAAK,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;GASzC;AAED,gFAAgF;AAChF,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM;;GAoC1D;AAED,8EAA8E;AAC9E,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM;;GA+BzC;AAED,kFAAkF;AAClF,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM;;GA+BvD;AAED,mFAAmF;AACnF,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;GA0BlE"}
@@ -3,7 +3,7 @@
3
3
  * Handles session creation, Redis caching, and email notifications.
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d8981916-8875-5f33-b907-7e722e9a3ce9")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="50950b99-0cf5-508a-86f4-0c11cb07851e")}catch(e){}}();
7
7
  import { SessionType } from "@prisma/client";
8
8
  import { TRPCError } from "@trpc/server";
9
9
  import { v4 as uuidv4 } from "uuid";
@@ -130,7 +130,10 @@ export async function resendVerificationEmail(email) {
130
130
  message: "User not found",
131
131
  });
132
132
  }
133
- await deleteSessionsByUserId(user.id);
133
+ // Do NOT delete the user's sessions here — resending a verification email
134
+ // must never log the account out of its active devices. A previously issued
135
+ // verification token simply expires on its own; issuing a fresh one is
136
+ // enough.
134
137
  const verificationToken = await createSession({
135
138
  id: uuidv4(),
136
139
  type: SessionType.EMAIL_VERIFICATION,
@@ -227,8 +230,11 @@ export async function resetPassword(token, password) {
227
230
  });
228
231
  }
229
232
  await updateUserPassword(session.userId, await hash(password, 10));
230
- await deleteSessionById(token);
233
+ // Revoke every session (AUTH + any other reset/verification tokens), not
234
+ // just the consumed reset token, so a stolen bearer token can't outlive a
235
+ // password reset.
236
+ await deleteSessionsByUserId(session.userId);
231
237
  return { success: true };
232
238
  }
233
239
  //# sourceMappingURL=auth.js.map
234
- //# debugId=d8981916-8875-5f33-b907-7e722e9a3ce9
240
+ //# debugId=50950b99-0cf5-508a-86f4-0c11cb07851e
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sources":["services/auth.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Auth service – user registration, login, logout, verification, and password reset.\n * Handles session creation, Redis caching, and email notifications.\n */\nimport { SessionType } from \"@prisma/client\";\nimport { TRPCError } from \"@trpc/server\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { compare, hash } from \"bcryptjs\";\nimport { sendMail } from \"../utils/email.js\";\nimport { prismaWrapper } from \"../utils/prismaWrapper.js\";\nimport { env } from \"../lib/config/env.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { CURRENT_TERMS_VERSION } from \"../lib/legal.js\";\nimport {\n findUserByUsername,\n findUserByUsernameOrEmail,\n findUserByEmail,\n findUserById,\n createUser,\n createSession,\n deleteSessionsByUserId,\n deleteShortLivedSessionsByUserId,\n deleteUserById,\n findSessionByIdAndType,\n findSessionByIdWithUserAndType,\n updateUserVerified,\n updateUserPassword,\n deleteSessionById,\n} from \"../models/auth.js\";\n\n/** Register a new user, create verification session, and send verification email. */\nexport async function register(input: {\n username: string;\n email: string;\n password: string;\n agreeToTerms: true;\n termsVersion: string;\n}) {\n const { username, email, password, termsVersion } = input;\n\n if (termsVersion !== CURRENT_TERMS_VERSION) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Please accept the current Terms of Service and Privacy Policy.\",\n });\n }\n\n const existingUser = await prismaWrapper.findFirst(\n () => findUserByUsernameOrEmail(username, email),\n \"checking for existing user during registration\"\n );\n\n if (existingUser && existingUser.verified) {\n if (existingUser.username === username) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: \"Username already exists\",\n });\n }\n if (existingUser.email === email) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: \"Email already exists\",\n });\n }\n } else if (existingUser && !existingUser.verified) {\n await prismaWrapper.deleteMany(\n () => deleteSessionsByUserId(existingUser.id),\n \"deleting existing sessions for unverified user\"\n );\n await prismaWrapper.delete(\n () => deleteUserById(existingUser.id),\n \"deleting unverified user\"\n );\n }\n\n const user = await prismaWrapper.create(\n async () =>\n createUser({\n username,\n email,\n password: await hash(password, 10),\n verified: false,\n termsAcceptedAt: new Date(),\n termsVersion,\n }),\n \"creating new user during registration\"\n );\n\n const verificationToken = await prismaWrapper.create(\n () =>\n createSession({\n id: uuidv4(),\n type: SessionType.EMAIL_VERIFICATION,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n }),\n \"creating verification token\"\n );\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Verify your email\",\n text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send verification email\", {\n email: user.email,\n err,\n });\n }\n\n return { user: { id: user.id, username: user.username } };\n}\n\n/** Authenticate user by username/password, create session. */\nexport async function login(input: { username: string; password: string }) {\n const { username, password } = input;\n\n const user = await findUserByUsername(username);\n if (!user) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Invalid username or password\",\n });\n }\n\n if (!(await compare(password, user.password))) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Invalid username or password\",\n });\n }\n\n if (!user.verified) {\n return {\n verified: false,\n user: { email: user.email },\n };\n }\n\n const session = await createSession({\n id: uuidv4(),\n type: SessionType.AUTH,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n });\n\n return {\n token: session.id,\n user: { id: user.id, username: user.username },\n };\n}\n\n/** Delete all sessions for a user (logs them out everywhere). */\nexport async function logout(userId: string) {\n await deleteSessionsByUserId(userId);\n return { success: true };\n}\n\n/** Verify user exists and return profile. Used for session validation. */\nexport async function check(userId: string) {\n const user = await findUserById(userId);\n if (!user) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"User not found\",\n });\n }\n return { user };\n}\n\n/** Resend verification email. Replaces existing sessions and sends new link. */\nexport async function resendVerificationEmail(email: string) {\n const user = await findUserByEmail(email);\n if (!user) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"User not found\",\n });\n }\n\n await deleteSessionsByUserId(user.id);\n\n const verificationToken = await createSession({\n id: uuidv4(),\n type: SessionType.EMAIL_VERIFICATION,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n });\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Verify your email\",\n text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send verification email\", {\n email: user.email,\n err,\n });\n }\n\n return { success: true };\n}\n\n/** Verify email via token. Marks user as verified and deletes the session. */\nexport async function verify(token: string) {\n const session = await findSessionByIdAndType(\n token,\n SessionType.EMAIL_VERIFICATION\n );\n if (!session) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Session not found\",\n });\n }\n\n if (session.expiresAt && session.expiresAt < new Date()) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Session expired\",\n });\n }\n\n if (!session.userId) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Invalid session state\",\n });\n }\n\n await updateUserVerified(session.userId, true);\n await deleteSessionById(token);\n return { success: true };\n}\n\n/** Send password reset email. Silently succeeds if email not found (security). */\nexport async function requestPasswordReset(email: string) {\n const user = await findUserByEmail(email);\n if (!user) {\n return { success: true };\n }\n\n const twoHoursFromNow = new Date(Date.now() + 1000 * 60 * 60 * 2);\n await deleteShortLivedSessionsByUserId(user.id, twoHoursFromNow);\n\n const resetToken = await createSession({\n id: uuidv4(),\n type: SessionType.PASSWORD_RESET,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60),\n });\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Reset your password\",\n text: `Click the link to reset your password: ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send password reset email\", {\n email: user.email,\n err,\n });\n }\n\n return { success: true };\n}\n\n/** Reset password using token from email. Validates token and updates password. */\nexport async function resetPassword(token: string, password: string) {\n const session = await findSessionByIdWithUserAndType(\n token,\n SessionType.PASSWORD_RESET\n );\n if (!session || !session.userId) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Invalid or expired reset token\",\n });\n }\n\n if (session.expiresAt && session.expiresAt < new Date()) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Reset token has expired\",\n });\n }\n\n await updateUserPassword(session.userId, await hash(password, 10));\n await deleteSessionById(token);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"d8981916-8875-5f33-b907-7e722e9a3ce9"}
1
+ {"version":3,"file":"auth.js","sources":["services/auth.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Auth service – user registration, login, logout, verification, and password reset.\n * Handles session creation, Redis caching, and email notifications.\n */\nimport { SessionType } from \"@prisma/client\";\nimport { TRPCError } from \"@trpc/server\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { compare, hash } from \"bcryptjs\";\nimport { sendMail } from \"../utils/email.js\";\nimport { prismaWrapper } from \"../utils/prismaWrapper.js\";\nimport { env } from \"../lib/config/env.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { CURRENT_TERMS_VERSION } from \"../lib/legal.js\";\nimport {\n findUserByUsername,\n findUserByUsernameOrEmail,\n findUserByEmail,\n findUserById,\n createUser,\n createSession,\n deleteSessionsByUserId,\n deleteShortLivedSessionsByUserId,\n deleteUserById,\n findSessionByIdAndType,\n findSessionByIdWithUserAndType,\n updateUserVerified,\n updateUserPassword,\n deleteSessionById,\n} from \"../models/auth.js\";\n\n/** Register a new user, create verification session, and send verification email. */\nexport async function register(input: {\n username: string;\n email: string;\n password: string;\n agreeToTerms: true;\n termsVersion: string;\n}) {\n const { username, email, password, termsVersion } = input;\n\n if (termsVersion !== CURRENT_TERMS_VERSION) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Please accept the current Terms of Service and Privacy Policy.\",\n });\n }\n\n const existingUser = await prismaWrapper.findFirst(\n () => findUserByUsernameOrEmail(username, email),\n \"checking for existing user during registration\"\n );\n\n if (existingUser && existingUser.verified) {\n if (existingUser.username === username) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: \"Username already exists\",\n });\n }\n if (existingUser.email === email) {\n throw new TRPCError({\n code: \"CONFLICT\",\n message: \"Email already exists\",\n });\n }\n } else if (existingUser && !existingUser.verified) {\n await prismaWrapper.deleteMany(\n () => deleteSessionsByUserId(existingUser.id),\n \"deleting existing sessions for unverified user\"\n );\n await prismaWrapper.delete(\n () => deleteUserById(existingUser.id),\n \"deleting unverified user\"\n );\n }\n\n const user = await prismaWrapper.create(\n async () =>\n createUser({\n username,\n email,\n password: await hash(password, 10),\n verified: false,\n termsAcceptedAt: new Date(),\n termsVersion,\n }),\n \"creating new user during registration\"\n );\n\n const verificationToken = await prismaWrapper.create(\n () =>\n createSession({\n id: uuidv4(),\n type: SessionType.EMAIL_VERIFICATION,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n }),\n \"creating verification token\"\n );\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Verify your email\",\n text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send verification email\", {\n email: user.email,\n err,\n });\n }\n\n return { user: { id: user.id, username: user.username } };\n}\n\n/** Authenticate user by username/password, create session. */\nexport async function login(input: { username: string; password: string }) {\n const { username, password } = input;\n\n const user = await findUserByUsername(username);\n if (!user) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Invalid username or password\",\n });\n }\n\n if (!(await compare(password, user.password))) {\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Invalid username or password\",\n });\n }\n\n if (!user.verified) {\n return {\n verified: false,\n user: { email: user.email },\n };\n }\n\n const session = await createSession({\n id: uuidv4(),\n type: SessionType.AUTH,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n });\n\n return {\n token: session.id,\n user: { id: user.id, username: user.username },\n };\n}\n\n/** Delete all sessions for a user (logs them out everywhere). */\nexport async function logout(userId: string) {\n await deleteSessionsByUserId(userId);\n return { success: true };\n}\n\n/** Verify user exists and return profile. Used for session validation. */\nexport async function check(userId: string) {\n const user = await findUserById(userId);\n if (!user) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"User not found\",\n });\n }\n return { user };\n}\n\n/** Resend verification email. Replaces existing sessions and sends new link. */\nexport async function resendVerificationEmail(email: string) {\n const user = await findUserByEmail(email);\n if (!user) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"User not found\",\n });\n }\n\n // Do NOT delete the user's sessions here — resending a verification email\n // must never log the account out of its active devices. A previously issued\n // verification token simply expires on its own; issuing a fresh one is\n // enough.\n\n const verificationToken = await createSession({\n id: uuidv4(),\n type: SessionType.EMAIL_VERIFICATION,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),\n });\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Verify your email\",\n text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send verification email\", {\n email: user.email,\n err,\n });\n }\n\n return { success: true };\n}\n\n/** Verify email via token. Marks user as verified and deletes the session. */\nexport async function verify(token: string) {\n const session = await findSessionByIdAndType(\n token,\n SessionType.EMAIL_VERIFICATION\n );\n if (!session) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Session not found\",\n });\n }\n\n if (session.expiresAt && session.expiresAt < new Date()) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Session expired\",\n });\n }\n\n if (!session.userId) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Invalid session state\",\n });\n }\n\n await updateUserVerified(session.userId, true);\n await deleteSessionById(token);\n return { success: true };\n}\n\n/** Send password reset email. Silently succeeds if email not found (security). */\nexport async function requestPasswordReset(email: string) {\n const user = await findUserByEmail(email);\n if (!user) {\n return { success: true };\n }\n\n const twoHoursFromNow = new Date(Date.now() + 1000 * 60 * 60 * 2);\n await deleteShortLivedSessionsByUserId(user.id, twoHoursFromNow);\n\n const resetToken = await createSession({\n id: uuidv4(),\n type: SessionType.PASSWORD_RESET,\n userId: user.id,\n expiresAt: new Date(Date.now() + 1000 * 60 * 60),\n });\n\n try {\n await sendMail({\n from: \"hello@studious.sh\",\n to: user.email,\n subject: \"Reset your password\",\n text: `Click the link to reset your password: ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`,\n });\n } catch (err) {\n logger.error(\"Failed to send password reset email\", {\n email: user.email,\n err,\n });\n }\n\n return { success: true };\n}\n\n/** Reset password using token from email. Validates token and updates password. */\nexport async function resetPassword(token: string, password: string) {\n const session = await findSessionByIdWithUserAndType(\n token,\n SessionType.PASSWORD_RESET\n );\n if (!session || !session.userId) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Invalid or expired reset token\",\n });\n }\n\n if (session.expiresAt && session.expiresAt < new Date()) {\n await deleteSessionById(token);\n throw new TRPCError({\n code: \"UNAUTHORIZED\",\n message: \"Reset token has expired\",\n });\n }\n\n await updateUserPassword(session.userId, await hash(password, 10));\n // Revoke every session (AUTH + any other reset/verification tokens), not\n // just the consumed reset token, so a stolen bearer token can't outlive a\n // password reset.\n await deleteSessionsByUserId(session.userId);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"50950b99-0cf5-508a-86f4-0c11cb07851e"}
@@ -44,6 +44,7 @@ export declare function listConversations(userId: string): Promise<{
44
44
  content: string;
45
45
  senderId: string;
46
46
  conversationId: string;
47
+ aiFeedback: import(".prisma/client").$Enums.AiMessageFeedback | null;
47
48
  runJobId: string | null;
48
49
  };
49
50
  unreadCount: number;
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"/","sources":["services/conversation.ts"],"names":[],"mappings":"AA0BA,gEAAgE;AAChE,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuDrD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;;GAY5E;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;KAe5E;AAED,gEAAgE;AAChE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EpB;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAavB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BvB;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAejB"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"/","sources":["services/conversation.ts"],"names":[],"mappings":"AA0BA,gEAAgE;AAChE,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAuDrD;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;;GAY5E;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;;;;;;;;KAe5E;AAED,gEAAgE;AAChE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,GAAG,OAAO,EACpB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,SAAS,EAAE,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EpB;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAavB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BvB;AAED,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;GAejB"}
@@ -1 +1 @@
1
- {"version":3,"file":"fees.d.ts","sourceRoot":"/","sources":["services/fees.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fees.d.ts","sourceRoot":"/","sources":["services/fees.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@
3
3
  * Payment gateway intentionally deferred; this is a manual ledger.
4
4
  */
5
5
 
6
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="17f46206-bcf6-5721-9d78-09801ff55dea")}catch(e){}}();
6
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fbc59022-10f0-5feb-9396-4ad26a37b74a")}catch(e){}}();
7
7
  import { TRPCError } from "@trpc/server";
8
8
  import { findActiveSchoolAdminMembership } from "../models/school.js";
9
9
  import { createFeeInvoices, createFeePayment, createFeePlan, getFeeInvoice, getFeePayment, getFeePlan, listActiveStudentIds, listFeeInvoices, listFeePlans, listPlanEnrollmentIds, listStudentBalances, setFeePlanEnrollments, sumPaymentsForInvoice, updateFeePlan, updateInvoiceStatus, } from "../models/fees.js";
@@ -310,8 +310,12 @@ export async function getReceiptPdf(userId, paymentId) {
310
310
  },
311
311
  {
312
312
  label: "Paid at",
313
+ // Format in UTC rather than a hardcoded country timezone — Studious is
314
+ // used internationally, and this matches the UTC basis already used
315
+ // for invoice month labels. A per-school timezone setting is the
316
+ // proper long-term home for this (see docs/audits bug audit §2).
313
317
  value: new Intl.DateTimeFormat("en-CA", {
314
- timeZone: "Asia/Kathmandu",
318
+ timeZone: "UTC",
315
319
  year: "numeric",
316
320
  month: "2-digit",
317
321
  day: "2-digit",
@@ -327,4 +331,4 @@ export async function getReceiptPdf(userId, paymentId) {
327
331
  };
328
332
  }
329
333
  //# sourceMappingURL=fees.js.map
330
- //# debugId=17f46206-bcf6-5721-9d78-09801ff55dea
334
+ //# debugId=fbc59022-10f0-5feb-9396-4ad26a37b74a