@studious-lms/server 1.15.0 → 1.16.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 (113) hide show
  1. package/.github/workflows/deploy.yml +1 -1
  2. package/dist/instrument.js +3 -3
  3. package/dist/instrument.js.map +1 -1
  4. package/dist/models/announcement.d.ts +0 -2
  5. package/dist/models/announcement.d.ts.map +1 -1
  6. package/dist/models/auth.d.ts +0 -8
  7. package/dist/models/auth.d.ts.map +1 -1
  8. package/dist/models/auth.js +2 -4
  9. package/dist/models/auth.js.map +1 -1
  10. package/dist/models/event.d.ts +0 -2
  11. package/dist/models/event.d.ts.map +1 -1
  12. package/dist/models/file.d.ts +0 -24
  13. package/dist/models/file.d.ts.map +1 -1
  14. package/dist/models/marketing.d.ts +15 -2
  15. package/dist/models/marketing.d.ts.map +1 -1
  16. package/dist/models/marketing.js +4 -12
  17. package/dist/models/marketing.js.map +1 -1
  18. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  19. package/dist/pipelines/aiLabChat.js +26 -34
  20. package/dist/pipelines/aiLabChat.js.map +1 -1
  21. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  22. package/dist/pipelines/gradeWorksheet.js +75 -36
  23. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  24. package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
  25. package/dist/pipelines/labChatPrompt.js +3 -6
  26. package/dist/pipelines/labChatPrompt.js.map +1 -1
  27. package/dist/routers/_app.d.ts +26 -222
  28. package/dist/routers/_app.d.ts.map +1 -1
  29. package/dist/routers/assignment.d.ts +0 -10
  30. package/dist/routers/assignment.d.ts.map +1 -1
  31. package/dist/routers/auth.d.ts +0 -2
  32. package/dist/routers/auth.d.ts.map +1 -1
  33. package/dist/routers/auth.js +2 -15
  34. package/dist/routers/auth.js.map +1 -1
  35. package/dist/routers/class.d.ts +0 -4
  36. package/dist/routers/class.d.ts.map +1 -1
  37. package/dist/routers/event.d.ts +0 -2
  38. package/dist/routers/event.d.ts.map +1 -1
  39. package/dist/routers/marketing.d.ts +13 -0
  40. package/dist/routers/marketing.d.ts.map +1 -1
  41. package/dist/routers/school.d.ts +0 -4
  42. package/dist/routers/school.d.ts.map +1 -1
  43. package/dist/routers/user.d.ts +0 -89
  44. package/dist/routers/user.d.ts.map +1 -1
  45. package/dist/routers/user.js +2 -19
  46. package/dist/routers/user.js.map +1 -1
  47. package/dist/seedDatabase.d.ts +0 -2
  48. package/dist/seedDatabase.d.ts.map +1 -1
  49. package/dist/services/aiChatStream.d.ts +0 -2
  50. package/dist/services/aiChatStream.d.ts.map +1 -1
  51. package/dist/services/aiChatStream.js +4 -31
  52. package/dist/services/aiChatStream.js.map +1 -1
  53. package/dist/services/aiProposalTools.d.ts.map +1 -1
  54. package/dist/services/aiProposalTools.js +7 -7
  55. package/dist/services/aiProposalTools.js.map +1 -1
  56. package/dist/services/aiTools.js +3 -3
  57. package/dist/services/aiTools.js.map +1 -1
  58. package/dist/services/assignment.d.ts +0 -10
  59. package/dist/services/assignment.d.ts.map +1 -1
  60. package/dist/services/auth.d.ts +0 -2
  61. package/dist/services/auth.d.ts.map +1 -1
  62. package/dist/services/auth.js +4 -13
  63. package/dist/services/auth.js.map +1 -1
  64. package/dist/services/class.d.ts +0 -4
  65. package/dist/services/class.d.ts.map +1 -1
  66. package/dist/services/event.d.ts +0 -2
  67. package/dist/services/event.d.ts.map +1 -1
  68. package/dist/services/marketing.d.ts +14 -1
  69. package/dist/services/marketing.d.ts.map +1 -1
  70. package/dist/services/marketing.js +5 -5
  71. package/dist/services/marketing.js.map +1 -1
  72. package/dist/services/openaiDocumentGen.d.ts +0 -12
  73. package/dist/services/openaiDocumentGen.d.ts.map +1 -1
  74. package/dist/services/openaiDocumentGen.js +63 -130
  75. package/dist/services/openaiDocumentGen.js.map +1 -1
  76. package/dist/services/school.d.ts +0 -4
  77. package/dist/services/school.d.ts.map +1 -1
  78. package/dist/utils/email.d.ts.map +1 -1
  79. package/dist/utils/email.js +3 -7
  80. package/dist/utils/email.js.map +1 -1
  81. package/package.json +1 -1
  82. package/prisma/schema.prisma +0 -3
  83. package/src/instrument.ts +1 -1
  84. package/src/models/auth.ts +0 -4
  85. package/src/models/marketing.ts +2 -10
  86. package/src/pipelines/aiLabChat.ts +28 -41
  87. package/src/pipelines/gradeWorksheet.ts +95 -38
  88. package/src/pipelines/labChatPrompt.ts +1 -4
  89. package/src/routers/auth.ts +0 -13
  90. package/src/routers/user.ts +0 -26
  91. package/src/services/aiChatStream.ts +3 -30
  92. package/src/services/aiProposalTools.ts +5 -6
  93. package/src/services/aiTools.ts +1 -1
  94. package/src/services/auth.ts +2 -14
  95. package/src/services/marketing.ts +3 -3
  96. package/src/services/openaiDocumentGen.ts +64 -146
  97. package/src/utils/email.ts +1 -5
  98. package/tests/integration/globalSetup.ts +18 -10
  99. package/tests/integration/routers/auth.test.ts +18 -13
  100. package/dist/lib/legal.d.ts +0 -3
  101. package/dist/lib/legal.d.ts.map +0 -1
  102. package/dist/lib/legal.js +0 -6
  103. package/dist/lib/legal.js.map +0 -1
  104. package/dist/services/userPrivacy.d.ts +0 -86
  105. package/dist/services/userPrivacy.d.ts.map +0 -1
  106. package/dist/services/userPrivacy.js +0 -184
  107. package/dist/services/userPrivacy.js.map +0 -1
  108. package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +0 -3
  109. package/src/lib/legal.ts +0 -2
  110. package/src/services/userPrivacy.ts +0 -196
  111. package/tests/helpers/registerInput.ts +0 -20
  112. package/tests/unit/services/aiChatStream.test.ts +0 -92
  113. package/tests/unit/services/openaiDocumentGen.test.ts +0 -65
@@ -3,38 +3,14 @@
3
3
  * Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).
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]="fcdb5dd4-2ded-545c-9a63-fd510590768f")}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]="cd65973d-ca13-5498-8d6b-ff056b093631")}catch(e){}}();
7
7
  import OpenAI from 'openai';
8
8
  import { logger } from '../utils/logger.js';
9
9
  const DOCUMENT_GEN_MODEL = 'gpt-4o-mini';
10
- /**
11
- * Extensions OpenAI Code Interpreter can read/write in its sandbox.
12
- * @see https://developers.openai.com/api/docs/guides/tools-code-interpreter
13
- */
14
10
  export const SUPPORTED_DOCUMENT_FORMATS = [
15
- 'pdf',
16
- 'docx',
17
- 'doc',
18
- 'xlsx',
19
- 'pptx',
20
- 'csv',
21
- 'md',
22
- 'html',
23
- 'txt',
24
- 'json',
25
- 'xml',
26
- 'tex',
27
- 'png',
28
- 'jpeg',
29
- 'jpg',
30
- 'gif',
31
- 'zip',
32
- 'tar',
33
- 'py',
34
- 'js',
35
- 'ts',
36
- 'css',
37
- 'sh',
11
+ 'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt', 'json',
12
+ 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar', 'py', 'js', 'ts',
13
+ 'css', 'sh',
38
14
  ];
39
15
  const FORMAT_MIME = {
40
16
  pdf: 'application/pdf',
@@ -62,29 +38,29 @@ const FORMAT_MIME = {
62
38
  sh: 'application/x-sh',
63
39
  };
64
40
  const FORMAT_HINTS = {
65
- pdf: 'fpdf2 or reportlab — structured document with headings and body text',
66
- docx: 'python-docx — Word document with headings and paragraphs',
67
- doc: 'python-docx saving as .doc if supported, otherwise .docx',
68
- xlsx: 'openpyxl or pandas — spreadsheet with labelled columns/rows',
69
- pptx: 'python-pptx — slides with title + bullet content per slide',
70
- csv: 'pandas or csv module — tabular data with headers',
71
- md: 'write the markdown file directly',
72
- html: 'write a complete, styled HTML page',
73
- txt: 'write plain text with clear section breaks',
74
- json: 'json.dumps with readable indentation',
75
- xml: 'well-formed XML document',
76
- tex: 'LaTeX source that compiles to the requested content',
77
- png: 'matplotlib or Pillow — chart, diagram, or visual',
78
- jpeg: 'matplotlib or Pillow — save as JPEG',
79
- jpg: 'matplotlib or Pillow — save as JPEG',
80
- gif: 'matplotlib or Pillow — save as GIF',
81
- zip: 'zipfile — bundle multiple related files if needed',
82
- tar: 'tarfile — archive if multiple files are required',
83
- py: 'valid Python source code file',
84
- js: 'valid JavaScript source file',
85
- ts: 'valid TypeScript source file',
86
- css: 'stylesheet file',
87
- sh: 'shell script file',
41
+ pdf: 'fpdf2 or reportlab',
42
+ docx: 'python-docx',
43
+ doc: 'python-docx',
44
+ xlsx: 'openpyxl or pandas',
45
+ pptx: 'python-pptx',
46
+ csv: 'pandas or csv module',
47
+ md: 'write markdown directly',
48
+ html: 'write a complete HTML page',
49
+ txt: 'write plain text',
50
+ json: 'json.dumps with indentation',
51
+ xml: 'well-formed XML',
52
+ tex: 'LaTeX source',
53
+ png: 'matplotlib or Pillow',
54
+ jpeg: 'matplotlib or Pillow',
55
+ jpg: 'matplotlib or Pillow',
56
+ gif: 'matplotlib or Pillow',
57
+ zip: 'zipfile',
58
+ tar: 'tarfile',
59
+ py: 'Python source',
60
+ js: 'JavaScript source',
61
+ ts: 'TypeScript source',
62
+ css: 'CSS stylesheet',
63
+ sh: 'shell script',
88
64
  };
89
65
  export function normalizeDocumentFormat(format) {
90
66
  const raw = (format ?? 'pdf').toLowerCase().replace(/^\./, '');
@@ -96,21 +72,10 @@ export function normalizeDocumentFormat(format) {
96
72
  export function mimeTypeForFormat(format) {
97
73
  return FORMAT_MIME[format];
98
74
  }
99
- export function extensionForFormat(format) {
100
- return format === 'jpg' ? 'jpg' : format;
101
- }
102
- /** Pull generated-file citations for a given extension out of a Responses output. */
103
75
  export function extractContainerGeneratedFiles(response, extension) {
76
+ const suffix = `.${extension}`;
104
77
  const seen = new Set();
105
78
  const files = [];
106
- const suffix = `.${extension}`;
107
- const add = (containerId, fileId, filename) => {
108
- const key = `${containerId}:${fileId}`;
109
- if (seen.has(key))
110
- return;
111
- seen.add(key);
112
- files.push({ containerId, fileId, filename });
113
- };
114
79
  for (const item of response.output ?? []) {
115
80
  if (item.type !== 'message')
116
81
  continue;
@@ -121,9 +86,17 @@ export function extractContainerGeneratedFiles(response, extension) {
121
86
  if (ann.type !== 'container_file_citation')
122
87
  continue;
123
88
  const name = ann.filename?.toLowerCase() ?? '';
124
- if (name.endsWith(suffix)) {
125
- add(ann.container_id, ann.file_id, ann.filename);
126
- }
89
+ if (!name.endsWith(suffix))
90
+ continue;
91
+ const key = `${ann.container_id}:${ann.file_id}`;
92
+ if (seen.has(key))
93
+ continue;
94
+ seen.add(key);
95
+ files.push({
96
+ containerId: ann.container_id,
97
+ fileId: ann.file_id,
98
+ filename: ann.filename,
99
+ });
127
100
  }
128
101
  }
129
102
  }
@@ -152,66 +125,37 @@ async function listContainerGeneratedFiles(client, containerId, extension) {
152
125
  }
153
126
  return files;
154
127
  }
155
- async function downloadContainerFile(client, file) {
156
- try {
157
- const res = await client.containers.files.content.retrieve(file.fileId, {
158
- container_id: file.containerId,
159
- });
160
- const bytes = await res.arrayBuffer();
161
- return Buffer.from(bytes);
162
- }
163
- catch (error) {
164
- logger.error('Failed to download container file', { error, file });
165
- return null;
166
- }
167
- }
168
128
  function safeBasename(title) {
169
- const base = title
129
+ return (title
170
130
  .replace(/[^a-zA-Z0-9\s\-_]/g, '')
171
131
  .replace(/\s+/g, '_')
172
- .substring(0, 60);
173
- return base || 'document';
174
- }
175
- function buildDocumentGenInstructions(format) {
176
- return [
177
- 'You are a document generator for K-12 and university teachers.',
178
- 'When given a title, output format, and source content:',
179
- '1. Use the python tool to create exactly ONE output file of the requested type.',
180
- `2. For this format use: ${FORMAT_HINTS[format]}.`,
181
- `3. You MUST output a ${format.toUpperCase()} file — not any other format.`,
182
- `4. The saved filename MUST end with .${extensionForFormat(format)}.`,
183
- '5. Structure content clearly — headings, sections, lists, tables/charts as appropriate.',
184
- '6. Include the document title prominently where the format allows.',
185
- '7. Put ONLY the educational content in the file — no meta commentary.',
186
- '8. You MUST save the file to disk in the working directory before you finish.',
187
- ...(format !== 'pdf'
188
- ? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]
189
- : []),
190
- ].join(' ');
132
+ .substring(0, 60) || 'document');
191
133
  }
192
134
  /** Generate a file via OpenAI Code Interpreter. */
193
135
  export async function generateDocumentWithOpenAI(input) {
194
136
  const client = new OpenAI();
195
137
  const format = normalizeDocumentFormat(input.format);
196
- const ext = extensionForFormat(format);
138
+ const ext = format === 'jpg' ? 'jpg' : format;
197
139
  const filename = `${safeBasename(input.title)}.${ext}`;
198
140
  try {
199
141
  const response = await client.responses.create({
200
142
  model: DOCUMENT_GEN_MODEL,
201
- instructions: buildDocumentGenInstructions(format),
202
- tools: [
203
- {
204
- type: 'code_interpreter',
205
- container: { type: 'auto' },
206
- },
207
- ],
143
+ instructions: [
144
+ 'You generate classroom-ready files for teachers using the python tool.',
145
+ `You MUST output a ${ext.toUpperCase()} file — not any other format.`,
146
+ `Use: ${FORMAT_HINTS[format]}.`,
147
+ `The saved filename MUST end with .${ext}.`,
148
+ 'Save exactly one output file with the requested filename.',
149
+ 'Include only educational content — no chat commentary.',
150
+ ...(format !== 'pdf' ? [`Do NOT create a PDF when ${ext.toUpperCase()} was requested.`] : []),
151
+ ].join(' '),
152
+ tools: [{ type: 'code_interpreter', container: { type: 'auto' } }],
208
153
  tool_choice: 'required',
209
154
  input: [
210
- `Create a ${ext.toUpperCase()} file using the python tool.`,
211
- `Output filename (exact): ${filename}`,
212
- `Output format: ${ext}`,
213
- `Document title: ${input.title}`,
214
- 'Source content (markdown or plain text — adapt to the output format):',
155
+ `Create a ${ext.toUpperCase()} file.`,
156
+ `Filename (exact): ${filename}`,
157
+ `Title: ${input.title}`,
158
+ 'Content:',
215
159
  input.content,
216
160
  ].join('\n\n'),
217
161
  });
@@ -223,18 +167,18 @@ export async function generateDocumentWithOpenAI(input) {
223
167
  }
224
168
  }
225
169
  if (matches.length === 0) {
226
- logger.error('OpenAI document gen produced no matching files', {
170
+ logger.error('OpenAI document gen: no output file', {
227
171
  title: input.title,
228
172
  format,
229
- outputTypes: (response.output ?? []).map((o) => o.type),
230
173
  });
231
174
  return null;
232
175
  }
233
176
  const preferred = matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??
234
177
  matches[0];
235
- const bytes = await downloadContainerFile(client, preferred);
236
- if (!bytes)
237
- return null;
178
+ const res = await client.containers.files.content.retrieve(preferred.fileId, {
179
+ container_id: preferred.containerId,
180
+ });
181
+ const bytes = Buffer.from(await res.arrayBuffer());
238
182
  return {
239
183
  bytes,
240
184
  filename: preferred.filename,
@@ -243,20 +187,9 @@ export async function generateDocumentWithOpenAI(input) {
243
187
  };
244
188
  }
245
189
  catch (error) {
246
- logger.error('OpenAI document generation failed', {
247
- error,
248
- title: input.title,
249
- format,
250
- });
190
+ logger.error('OpenAI document generation failed', { error, title: input.title, format });
251
191
  return null;
252
192
  }
253
193
  }
254
- /** @deprecated Use {@link generateDocumentWithOpenAI} */
255
- export async function generatePdfWithOpenAI(input) {
256
- const result = await generateDocumentWithOpenAI({ ...input, format: 'pdf' });
257
- return result?.bytes ?? null;
258
- }
259
- /** @deprecated Use {@link extractContainerGeneratedFiles} */
260
- export const extractContainerPdfFiles = (response) => extractContainerGeneratedFiles(response, 'pdf');
261
194
  //# sourceMappingURL=openaiDocumentGen.js.map
262
- //# debugId=fcdb5dd4-2ded-545c-9a63-fd510590768f
195
+ //# debugId=cd65973d-ca13-5498-8d6b-ff056b093631
@@ -1 +1 @@
1
- {"version":3,"file":"openaiDocumentGen.js","sources":["services/openaiDocumentGen.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Generate files via OpenAI Code Interpreter (Responses API).\n * Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).\n */\nimport OpenAI from 'openai';\nimport type { Response } from 'openai/resources/responses/responses.js';\nimport { logger } from '../utils/logger.js';\n\nconst DOCUMENT_GEN_MODEL = 'gpt-4o-mini';\n\n/**\n * Extensions OpenAI Code Interpreter can read/write in its sandbox.\n * @see https://developers.openai.com/api/docs/guides/tools-code-interpreter\n */\nexport const SUPPORTED_DOCUMENT_FORMATS = [\n 'pdf',\n 'docx',\n 'doc',\n 'xlsx',\n 'pptx',\n 'csv',\n 'md',\n 'html',\n 'txt',\n 'json',\n 'xml',\n 'tex',\n 'png',\n 'jpeg',\n 'jpg',\n 'gif',\n 'zip',\n 'tar',\n 'py',\n 'js',\n 'ts',\n 'css',\n 'sh',\n] as const;\n\nexport type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];\n\nconst FORMAT_MIME: Record<DocumentFormat, string> = {\n pdf: 'application/pdf',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n doc: 'application/msword',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n csv: 'text/csv',\n md: 'text/markdown',\n html: 'text/html',\n txt: 'text/plain',\n json: 'application/json',\n xml: 'application/xml',\n tex: 'text/x-tex',\n png: 'image/png',\n jpeg: 'image/jpeg',\n jpg: 'image/jpeg',\n gif: 'image/gif',\n zip: 'application/zip',\n tar: 'application/x-tar',\n py: 'text/x-python',\n js: 'text/javascript',\n ts: 'application/typescript',\n css: 'text/css',\n sh: 'application/x-sh',\n};\n\nconst FORMAT_HINTS: Record<DocumentFormat, string> = {\n pdf: 'fpdf2 or reportlab — structured document with headings and body text',\n docx: 'python-docx — Word document with headings and paragraphs',\n doc: 'python-docx saving as .doc if supported, otherwise .docx',\n xlsx: 'openpyxl or pandas — spreadsheet with labelled columns/rows',\n pptx: 'python-pptx — slides with title + bullet content per slide',\n csv: 'pandas or csv module — tabular data with headers',\n md: 'write the markdown file directly',\n html: 'write a complete, styled HTML page',\n txt: 'write plain text with clear section breaks',\n json: 'json.dumps with readable indentation',\n xml: 'well-formed XML document',\n tex: 'LaTeX source that compiles to the requested content',\n png: 'matplotlib or Pillow — chart, diagram, or visual',\n jpeg: 'matplotlib or Pillow — save as JPEG',\n jpg: 'matplotlib or Pillow — save as JPEG',\n gif: 'matplotlib or Pillow — save as GIF',\n zip: 'zipfile — bundle multiple related files if needed',\n tar: 'tarfile — archive if multiple files are required',\n py: 'valid Python source code file',\n js: 'valid JavaScript source file',\n ts: 'valid TypeScript source file',\n css: 'stylesheet file',\n sh: 'shell script file',\n};\n\nexport interface GenerateDocumentInput {\n title: string;\n /** Markdown or plain-text source material for the output file. */\n content: string;\n /** Output format. Defaults to pdf. */\n format?: DocumentFormat | string;\n}\n\nexport interface GeneratedDocumentResult {\n bytes: Buffer;\n filename: string;\n mimeType: string;\n format: DocumentFormat;\n}\n\nexport interface ContainerGeneratedFile {\n containerId: string;\n fileId: string;\n filename: string;\n}\n\nexport function normalizeDocumentFormat(\n format?: string | null,\n): DocumentFormat {\n const raw = (format ?? 'pdf').toLowerCase().replace(/^\\./, '');\n if ((SUPPORTED_DOCUMENT_FORMATS as readonly string[]).includes(raw)) {\n return raw as DocumentFormat;\n }\n return 'pdf';\n}\n\nexport function mimeTypeForFormat(format: DocumentFormat): string {\n return FORMAT_MIME[format];\n}\n\nexport function extensionForFormat(format: DocumentFormat): string {\n return format === 'jpg' ? 'jpg' : format;\n}\n\n/** Pull generated-file citations for a given extension out of a Responses output. */\nexport function extractContainerGeneratedFiles(\n response: Response,\n extension: DocumentFormat,\n): ContainerGeneratedFile[] {\n const seen = new Set<string>();\n const files: ContainerGeneratedFile[] = [];\n const suffix = `.${extension}`;\n\n const add = (containerId: string, fileId: string, filename: string) => {\n const key = `${containerId}:${fileId}`;\n if (seen.has(key)) return;\n seen.add(key);\n files.push({ containerId, fileId, filename });\n };\n\n for (const item of response.output ?? []) {\n if (item.type !== 'message') continue;\n for (const block of item.content ?? []) {\n if (block.type !== 'output_text') continue;\n for (const ann of block.annotations ?? []) {\n if (ann.type !== 'container_file_citation') continue;\n const name = ann.filename?.toLowerCase() ?? '';\n if (name.endsWith(suffix)) {\n add(ann.container_id, ann.file_id, ann.filename);\n }\n }\n }\n }\n\n return files;\n}\n\nfunction containerIdFromResponse(response: Response): string | undefined {\n for (const item of response.output ?? []) {\n if (item.type === 'code_interpreter_call' && item.container_id) {\n return item.container_id;\n }\n }\n return undefined;\n}\n\nasync function listContainerGeneratedFiles(\n client: OpenAI,\n containerId: string,\n extension: DocumentFormat,\n): Promise<ContainerGeneratedFile[]> {\n const suffix = `.${extension}`;\n const files: ContainerGeneratedFile[] = [];\n for await (const file of client.containers.files.list(containerId)) {\n const path = file.path?.toLowerCase() ?? '';\n if (path.endsWith(suffix) && file.source === 'assistant') {\n files.push({\n containerId,\n fileId: file.id,\n filename: file.path.split('/').pop() ?? file.path,\n });\n }\n }\n return files;\n}\n\nasync function downloadContainerFile(\n client: OpenAI,\n file: ContainerGeneratedFile,\n): Promise<Buffer | null> {\n try {\n const res = await client.containers.files.content.retrieve(file.fileId, {\n container_id: file.containerId,\n });\n const bytes = await res.arrayBuffer();\n return Buffer.from(bytes);\n } catch (error) {\n logger.error('Failed to download container file', { error, file });\n return null;\n }\n}\n\nfunction safeBasename(title: string): string {\n const base = title\n .replace(/[^a-zA-Z0-9\\s\\-_]/g, '')\n .replace(/\\s+/g, '_')\n .substring(0, 60);\n return base || 'document';\n}\n\nfunction buildDocumentGenInstructions(format: DocumentFormat): string {\n return [\n 'You are a document generator for K-12 and university teachers.',\n 'When given a title, output format, and source content:',\n '1. Use the python tool to create exactly ONE output file of the requested type.',\n `2. For this format use: ${FORMAT_HINTS[format]}.`,\n `3. You MUST output a ${format.toUpperCase()} file — not any other format.`,\n `4. The saved filename MUST end with .${extensionForFormat(format)}.`,\n '5. Structure content clearly — headings, sections, lists, tables/charts as appropriate.',\n '6. Include the document title prominently where the format allows.',\n '7. Put ONLY the educational content in the file — no meta commentary.',\n '8. You MUST save the file to disk in the working directory before you finish.',\n ...(format !== 'pdf'\n ? [`9. Do NOT create a PDF when ${format.toUpperCase()} was requested.`]\n : []),\n ].join(' ');\n}\n\n/** Generate a file via OpenAI Code Interpreter. */\nexport async function generateDocumentWithOpenAI(\n input: GenerateDocumentInput,\n): Promise<GeneratedDocumentResult | null> {\n const client = new OpenAI();\n const format = normalizeDocumentFormat(input.format);\n const ext = extensionForFormat(format);\n const filename = `${safeBasename(input.title)}.${ext}`;\n\n try {\n const response = await client.responses.create({\n model: DOCUMENT_GEN_MODEL,\n instructions: buildDocumentGenInstructions(format),\n tools: [\n {\n type: 'code_interpreter',\n container: { type: 'auto' },\n },\n ],\n tool_choice: 'required',\n input: [\n `Create a ${ext.toUpperCase()} file using the python tool.`,\n `Output filename (exact): ${filename}`,\n `Output format: ${ext}`,\n `Document title: ${input.title}`,\n 'Source content (markdown or plain text — adapt to the output format):',\n input.content,\n ].join('\\n\\n'),\n });\n\n let matches = extractContainerGeneratedFiles(response, format);\n\n if (matches.length === 0) {\n const containerId = containerIdFromResponse(response);\n if (containerId) {\n matches = await listContainerGeneratedFiles(client, containerId, format);\n }\n }\n\n if (matches.length === 0) {\n logger.error('OpenAI document gen produced no matching files', {\n title: input.title,\n format,\n outputTypes: (response.output ?? []).map((o) => o.type),\n });\n return null;\n }\n\n const preferred =\n matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??\n matches[0];\n\n const bytes = await downloadContainerFile(client, preferred);\n if (!bytes) return null;\n\n return {\n bytes,\n filename: preferred.filename,\n mimeType: mimeTypeForFormat(format),\n format,\n };\n } catch (error) {\n logger.error('OpenAI document generation failed', {\n error,\n title: input.title,\n format,\n });\n return null;\n }\n}\n\n/** @deprecated Use {@link generateDocumentWithOpenAI} */\nexport async function generatePdfWithOpenAI(\n input: Omit<GenerateDocumentInput, 'format'>,\n): Promise<Buffer | null> {\n const result = await generateDocumentWithOpenAI({ ...input, format: 'pdf' });\n return result?.bytes ?? null;\n}\n\n/** @deprecated Use {@link extractContainerGeneratedFiles} */\nexport const extractContainerPdfFiles = (response: Response) =>\n extractContainerGeneratedFiles(response, 'pdf');\n"],"names":[],"mappings":"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","debug_id":"fcdb5dd4-2ded-545c-9a63-fd510590768f"}
1
+ {"version":3,"file":"openaiDocumentGen.js","sources":["services/openaiDocumentGen.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Generate files via OpenAI Code Interpreter (Responses API).\n * Supports every format the sandbox can produce (pdf, docx, xlsx, pptx, …).\n */\nimport OpenAI from 'openai';\nimport type { Response } from 'openai/resources/responses/responses.js';\nimport { logger } from '../utils/logger.js';\n\nconst DOCUMENT_GEN_MODEL = 'gpt-4o-mini';\n\nexport const SUPPORTED_DOCUMENT_FORMATS = [\n 'pdf', 'docx', 'doc', 'xlsx', 'pptx', 'csv', 'md', 'html', 'txt', 'json',\n 'xml', 'tex', 'png', 'jpeg', 'jpg', 'gif', 'zip', 'tar', 'py', 'js', 'ts',\n 'css', 'sh',\n] as const;\n\nexport type DocumentFormat = (typeof SUPPORTED_DOCUMENT_FORMATS)[number];\n\nconst FORMAT_MIME: Record<DocumentFormat, string> = {\n pdf: 'application/pdf',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n doc: 'application/msword',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n csv: 'text/csv',\n md: 'text/markdown',\n html: 'text/html',\n txt: 'text/plain',\n json: 'application/json',\n xml: 'application/xml',\n tex: 'text/x-tex',\n png: 'image/png',\n jpeg: 'image/jpeg',\n jpg: 'image/jpeg',\n gif: 'image/gif',\n zip: 'application/zip',\n tar: 'application/x-tar',\n py: 'text/x-python',\n js: 'text/javascript',\n ts: 'application/typescript',\n css: 'text/css',\n sh: 'application/x-sh',\n};\n\nconst FORMAT_HINTS: Record<DocumentFormat, string> = {\n pdf: 'fpdf2 or reportlab',\n docx: 'python-docx',\n doc: 'python-docx',\n xlsx: 'openpyxl or pandas',\n pptx: 'python-pptx',\n csv: 'pandas or csv module',\n md: 'write markdown directly',\n html: 'write a complete HTML page',\n txt: 'write plain text',\n json: 'json.dumps with indentation',\n xml: 'well-formed XML',\n tex: 'LaTeX source',\n png: 'matplotlib or Pillow',\n jpeg: 'matplotlib or Pillow',\n jpg: 'matplotlib or Pillow',\n gif: 'matplotlib or Pillow',\n zip: 'zipfile',\n tar: 'tarfile',\n py: 'Python source',\n js: 'JavaScript source',\n ts: 'TypeScript source',\n css: 'CSS stylesheet',\n sh: 'shell script',\n};\n\nexport interface GenerateDocumentInput {\n title: string;\n content: string;\n format?: DocumentFormat | string;\n}\n\nexport interface GeneratedDocumentResult {\n bytes: Buffer;\n filename: string;\n mimeType: string;\n format: DocumentFormat;\n}\n\nexport interface ContainerGeneratedFile {\n containerId: string;\n fileId: string;\n filename: string;\n}\n\nexport function normalizeDocumentFormat(format?: string | null): DocumentFormat {\n const raw = (format ?? 'pdf').toLowerCase().replace(/^\\./, '');\n if ((SUPPORTED_DOCUMENT_FORMATS as readonly string[]).includes(raw)) {\n return raw as DocumentFormat;\n }\n return 'pdf';\n}\n\nexport function mimeTypeForFormat(format: DocumentFormat): string {\n return FORMAT_MIME[format];\n}\n\nexport function extractContainerGeneratedFiles(\n response: Response,\n extension: DocumentFormat,\n): ContainerGeneratedFile[] {\n const suffix = `.${extension}`;\n const seen = new Set<string>();\n const files: ContainerGeneratedFile[] = [];\n\n for (const item of response.output ?? []) {\n if (item.type !== 'message') continue;\n for (const block of item.content ?? []) {\n if (block.type !== 'output_text') continue;\n for (const ann of block.annotations ?? []) {\n if (ann.type !== 'container_file_citation') continue;\n const name = ann.filename?.toLowerCase() ?? '';\n if (!name.endsWith(suffix)) continue;\n const key = `${ann.container_id}:${ann.file_id}`;\n if (seen.has(key)) continue;\n seen.add(key);\n files.push({\n containerId: ann.container_id,\n fileId: ann.file_id,\n filename: ann.filename,\n });\n }\n }\n }\n return files;\n}\n\nfunction containerIdFromResponse(response: Response): string | undefined {\n for (const item of response.output ?? []) {\n if (item.type === 'code_interpreter_call' && item.container_id) {\n return item.container_id;\n }\n }\n return undefined;\n}\n\nasync function listContainerGeneratedFiles(\n client: OpenAI,\n containerId: string,\n extension: DocumentFormat,\n): Promise<ContainerGeneratedFile[]> {\n const suffix = `.${extension}`;\n const files: ContainerGeneratedFile[] = [];\n for await (const file of client.containers.files.list(containerId)) {\n const path = file.path?.toLowerCase() ?? '';\n if (path.endsWith(suffix) && file.source === 'assistant') {\n files.push({\n containerId,\n fileId: file.id,\n filename: file.path.split('/').pop() ?? file.path,\n });\n }\n }\n return files;\n}\n\nfunction safeBasename(title: string): string {\n return (\n title\n .replace(/[^a-zA-Z0-9\\s\\-_]/g, '')\n .replace(/\\s+/g, '_')\n .substring(0, 60) || 'document'\n );\n}\n\n/** Generate a file via OpenAI Code Interpreter. */\nexport async function generateDocumentWithOpenAI(\n input: GenerateDocumentInput,\n): Promise<GeneratedDocumentResult | null> {\n const client = new OpenAI();\n const format = normalizeDocumentFormat(input.format);\n const ext = format === 'jpg' ? 'jpg' : format;\n const filename = `${safeBasename(input.title)}.${ext}`;\n\n try {\n const response = await client.responses.create({\n model: DOCUMENT_GEN_MODEL,\n instructions: [\n 'You generate classroom-ready files for teachers using the python tool.',\n `You MUST output a ${ext.toUpperCase()} file — not any other format.`,\n `Use: ${FORMAT_HINTS[format]}.`,\n `The saved filename MUST end with .${ext}.`,\n 'Save exactly one output file with the requested filename.',\n 'Include only educational content — no chat commentary.',\n ...(format !== 'pdf' ? [`Do NOT create a PDF when ${ext.toUpperCase()} was requested.`] : []),\n ].join(' '),\n tools: [{ type: 'code_interpreter', container: { type: 'auto' } }],\n tool_choice: 'required',\n input: [\n `Create a ${ext.toUpperCase()} file.`,\n `Filename (exact): ${filename}`,\n `Title: ${input.title}`,\n 'Content:',\n input.content,\n ].join('\\n\\n'),\n });\n\n let matches = extractContainerGeneratedFiles(response, format);\n if (matches.length === 0) {\n const containerId = containerIdFromResponse(response);\n if (containerId) {\n matches = await listContainerGeneratedFiles(client, containerId, format);\n }\n }\n\n if (matches.length === 0) {\n logger.error('OpenAI document gen: no output file', {\n title: input.title,\n format,\n });\n return null;\n }\n\n const preferred =\n matches.find((f) => f.filename.toLowerCase() === filename.toLowerCase()) ??\n matches[0];\n\n const res = await client.containers.files.content.retrieve(preferred.fileId, {\n container_id: preferred.containerId,\n });\n const bytes = Buffer.from(await res.arrayBuffer());\n\n return {\n bytes,\n filename: preferred.filename,\n mimeType: mimeTypeForFormat(format),\n format,\n };\n } catch (error) {\n logger.error('OpenAI document generation failed', { error, title: input.title, format });\n return null;\n }\n}\n"],"names":[],"mappings":"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","debug_id":"cd65973d-ca13-5498-8d6b-ff056b093631"}
@@ -162,8 +162,6 @@ export declare function createSchoolClass(userId: string, input: {
162
162
  email: string;
163
163
  password: string;
164
164
  verified: boolean;
165
- termsAcceptedAt: Date | null;
166
- termsVersion: string | null;
167
165
  schoolId: string | null;
168
166
  }[];
169
167
  students: {
@@ -172,8 +170,6 @@ export declare function createSchoolClass(userId: string, input: {
172
170
  email: string;
173
171
  password: string;
174
172
  verified: boolean;
175
- termsAcceptedAt: Date | null;
176
- termsVersion: string | null;
177
173
  schoolId: string | null;
178
174
  }[];
179
175
  } & {
@@ -1 +1 @@
1
- {"version":3,"file":"school.d.ts","sourceRoot":"/","sources":["services/school.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"school.d.ts","sourceRoot":"/","sources":["services/school.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"email.d.ts","sourceRoot":"/","sources":["utils/email.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAGF,eAAO,MAAM,SAAS,oJAQlB,CAAC;AAGL,eAAO,MAAM,QAAQ,GAAU,6BAA6B,aAAa,kBAiBxE,CAAC"}
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"/","sources":["utils/email.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,UAAU,MAAM,YAAY,CAAC;AAIpC,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAGF,eAAO,MAAM,SAAS,oJAQlB,CAAC;AAGL,eAAO,MAAM,QAAQ,GAAU,6BAA6B,aAAa,kBAaxE,CAAC"}
@@ -2,7 +2,7 @@
2
2
  * Email util – nodemailer transport and sendMail wrapper. Supports EMAIL_DRY_RUN.
3
3
  */
4
4
 
5
- !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]="36743aa0-49dd-57e9-9c77-ec5d58404747")}catch(e){}}();
5
+ !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]="3fa5b5eb-3cd7-5eba-a26d-01b403a6ee46")}catch(e){}}();
6
6
  import nodemailer from 'nodemailer';
7
7
  import { env } from '../lib/config/env.js';
8
8
  import { logger } from './logger.js';
@@ -18,11 +18,7 @@ export const transport = nodemailer.createTransport({
18
18
  export const sendMail = async ({ from, to, subject, text }) => {
19
19
  // Wrapper function for sending emails
20
20
  if (env.EMAIL_DRY_RUN == "true") {
21
- logger.info("Email dry run enabled. Would have sent email", {
22
- to,
23
- from,
24
- subject,
25
- });
21
+ logger.info(`Email dry run enabled. Would have sent email to ${to} from ${from} with subject ${subject} and text ${text}`);
26
22
  return;
27
23
  }
28
24
  await transport.sendMail({
@@ -33,4 +29,4 @@ export const sendMail = async ({ from, to, subject, text }) => {
33
29
  });
34
30
  };
35
31
  //# sourceMappingURL=email.js.map
36
- //# debugId=36743aa0-49dd-57e9-9c77-ec5d58404747
32
+ //# debugId=3fa5b5eb-3cd7-5eba-a26d-01b403a6ee46
@@ -1 +1 @@
1
- {"version":3,"file":"email.js","sources":["utils/email.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Email util – nodemailer transport and sendMail wrapper. Supports EMAIL_DRY_RUN.\n */\nimport nodemailer from 'nodemailer';\nimport { env } from '../lib/config/env.js';\nimport { logger } from './logger.js';\n\ntype sendMailProps = {\n from: string;\n to: string;\n subject: string;\n text: string;\n};\n\n\nexport const transport = nodemailer.createTransport({\n host: env.EMAIL_HOST,\n port: env.EMAIL_PORT,\n secure: false,\n auth: {\n user: env.EMAIL_USER,\n pass: env.EMAIL_PASS,\n },\n });\n\n\nexport const sendMail = async ({ from, to, subject, text }: sendMailProps) => {\n // Wrapper function for sending emails\n if (env.EMAIL_DRY_RUN == \"true\") {\n logger.info(\"Email dry run enabled. Would have sent email\", {\n to,\n from,\n subject,\n });\n return;\n }\n \n await transport.sendMail({\n from: `\"Studious\" <${from}>`,\n to,\n subject,\n text,\n });\n};\n"],"names":[],"mappings":"AAAA;;GAEG;;;AACH,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAUrC,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC,eAAe,CAAC;IAChD,IAAI,EAAE,GAAG,CAAC,UAAU;IACpB,IAAI,EAAE,GAAG,CAAC,UAAU;IACpB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,GAAG,CAAC,UAAU;QACpB,IAAI,EAAE,GAAG,CAAC,UAAU;KACrB;CACF,CAAC,CAAC;AAGL,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAiB,EAAE,EAAE;IAC3E,sCAAsC;IACtC,IAAI,GAAG,CAAC,aAAa,IAAI,MAAM,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,8CAA8C,EAAE;YAC1D,EAAE;YACF,IAAI;YACJ,OAAO;SACR,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,SAAS,CAAC,QAAQ,CAAC;QACvB,IAAI,EAAE,eAAe,IAAI,GAAG;QAC5B,EAAE;QACF,OAAO;QACP,IAAI;KACL,CAAC,CAAC;AACL,CAAC,CAAC","debug_id":"36743aa0-49dd-57e9-9c77-ec5d58404747"}
1
+ {"version":3,"file":"email.js","sources":["utils/email.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Email util – nodemailer transport and sendMail wrapper. Supports EMAIL_DRY_RUN.\n */\nimport nodemailer from 'nodemailer';\nimport { env } from '../lib/config/env.js';\nimport { logger } from './logger.js';\n\ntype sendMailProps = {\n from: string;\n to: string;\n subject: string;\n text: string;\n};\n\n\nexport const transport = nodemailer.createTransport({\n host: env.EMAIL_HOST,\n port: env.EMAIL_PORT,\n secure: false,\n auth: {\n user: env.EMAIL_USER,\n pass: env.EMAIL_PASS,\n },\n });\n\n\nexport const sendMail = async ({ from, to, subject, text }: sendMailProps) => {\n // Wrapper function for sending emails\n if (env.EMAIL_DRY_RUN == \"true\") {\n logger.info(`Email dry run enabled. Would have sent email to ${to} from ${from} with subject ${subject} and text ${text}`);\n return;\n }\n \n await transport.sendMail({\n from: `\"Studious\" <${from}>`,\n to,\n subject,\n text,\n });\n};\n"],"names":[],"mappings":"AAAA;;GAEG;;;AACH,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAUrC,MAAM,CAAC,MAAM,SAAS,GAAG,UAAU,CAAC,eAAe,CAAC;IAChD,IAAI,EAAE,GAAG,CAAC,UAAU;IACpB,IAAI,EAAE,GAAG,CAAC,UAAU;IACpB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,GAAG,CAAC,UAAU;QACpB,IAAI,EAAE,GAAG,CAAC,UAAU;KACrB;CACF,CAAC,CAAC;AAGL,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAiB,EAAE,EAAE;IAC3E,sCAAsC;IACtC,IAAI,GAAG,CAAC,aAAa,IAAI,MAAM,EAAE,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,mDAAmD,EAAE,SAAS,IAAI,iBAAiB,OAAO,aAAa,IAAI,EAAE,CAAC,CAAC;QAC3H,OAAO;IACT,CAAC;IAED,MAAM,SAAS,CAAC,QAAQ,CAAC;QACvB,IAAI,EAAE,eAAe,IAAI,GAAG;QAC5B,EAAE;QACF,OAAO;QACP,IAAI;KACL,CAAC,CAAC;AACL,CAAC,CAAC","debug_id":"3fa5b5eb-3cd7-5eba-a26d-01b403a6ee46"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -196,9 +196,6 @@ model User {
196
196
  profile UserProfile?
197
197
  verified Boolean @default(false)
198
198
 
199
- termsAcceptedAt DateTime?
200
- termsVersion String?
201
-
202
199
  teacherIn Class[] @relation("UserTeacherToClass")
203
200
  studentIn Class[] @relation("UserStudentToClass")
204
201
 
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: false,
11
+ sendDefaultPii: true,
12
12
  // @todo: disable in test environment
13
13
  enabled: true, // Explicitly disable in test environment
14
14
  });
@@ -102,8 +102,6 @@ export function createUser(data: {
102
102
  email: string;
103
103
  password: string;
104
104
  verified?: boolean;
105
- termsAcceptedAt?: Date;
106
- termsVersion?: string;
107
105
  }) {
108
106
  return prisma.user.create({
109
107
  data: {
@@ -111,8 +109,6 @@ export function createUser(data: {
111
109
  email: data.email,
112
110
  password: data.password,
113
111
  verified: data.verified ?? false,
114
- termsAcceptedAt: data.termsAcceptedAt,
115
- termsVersion: data.termsVersion,
116
112
  profile: { create: {} },
117
113
  },
118
114
  select: {
@@ -27,18 +27,10 @@ export function createSchoolDevelopmentProgram(data: {
27
27
  });
28
28
  }
29
29
 
30
- /** Public status lookup — omits contact PII (email, phone, address). */
31
- export function findSchoolDevelopmentProgramStatusById(id: string) {
30
+ /** @returns School development program by ID. */
31
+ export function findSchoolDevelopmentProgramById(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
- },
42
34
  });
43
35
  }
44
36
 
@@ -13,18 +13,15 @@ import {
13
13
  import { logger } from "../utils/logger.js";
14
14
  import { v4 } from "uuid";
15
15
  import { bucket, getSignedUrl } from "../lib/googleCloudStorage.js";
16
- import {
17
- generateDocumentWithOpenAI,
18
- normalizeDocumentFormat,
19
- } from "../services/openaiDocumentGen.js";
16
+ import { generateDocumentWithOpenAI } from "../services/openaiDocumentGen.js";
20
17
  import { inferDocumentFormat } from "../lib/inferDocumentFormat.js";
18
+ import { normalizeDocumentFormat } from "../services/openaiDocumentGen.js";
21
19
  import { broadcastTextChunks, broadcastStreamComplete } from "../utils/streamBroadcast.js";
22
20
  import { buildLabChatAgentMessages } from "./labChatPrompt.js";
23
21
  import type { ConversationMessage } from "./labChatPrompt.js";
24
22
  import {
25
23
  applyResolvedFormatsToStreamParts,
26
24
  enrichStreamPartsWithGeneratedFiles,
27
- finalizeDeferredToolParts,
28
25
  streamChatResponse,
29
26
  type GeneratedDocFileLink,
30
27
  type StreamPart,
@@ -558,10 +555,9 @@ export const generateAndSendLabIntroduction = async (
558
555
  const generatedDocFiles: GeneratedDocFileLink[] = [];
559
556
  const resolvedDocs: Array<{ title: string; format: string }> = [];
560
557
 
561
- for (let i = 0; i < proposals.docs.length; i++) {
562
- const doc = proposals.docs[i];
558
+ for (const doc of proposals.docs) {
563
559
  if (!doc.title || !doc.content?.trim()) {
564
- logger.error(`Document ${i + 1} is missing title or content`, { labChatId, doc });
560
+ logger.error('Document missing title or content', { labChatId, doc });
565
561
  continue;
566
562
  }
567
563
 
@@ -589,10 +585,10 @@ export const generateAndSendLabIntroduction = async (
589
585
  format,
590
586
  });
591
587
  if (!generated) {
592
- logger.error(`Document ${i + 1} generation returned null`, {
588
+ logger.error('OpenAI document generation returned null', {
593
589
  labChatId,
594
590
  title: doc.title,
595
- format,
591
+ format: doc.format,
596
592
  });
597
593
  continue;
598
594
  }
@@ -604,18 +600,10 @@ export const generateAndSendLabIntroduction = async (
604
600
  const filename = `${sanitizedTitle}_${v4().substring(0, 8)}.${generated.format}`;
605
601
  const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
606
602
 
607
- logger.info(`Document ${i + 1} generated successfully`, {
608
- labChatId,
609
- title: doc.title,
610
- format: generated.format,
611
- });
612
-
613
603
  await bucket.file(filePath).save(generated.bytes, {
614
604
  metadata: { contentType: generated.mimeType },
615
605
  });
616
606
 
617
- logger.info(`Document ${i + 1} uploaded successfully`, { labChatId, filename });
618
-
619
607
  const file = await prisma.file.create({
620
608
  data: {
621
609
  name: filename,
@@ -638,7 +626,7 @@ export const generateAndSendLabIntroduction = async (
638
626
  format: generated.format,
639
627
  });
640
628
  } catch (docError) {
641
- logger.error(`Document generation failed for item ${i + 1}:`, {
629
+ logger.error('Document generation failed', {
642
630
  error: docError instanceof Error ? {
643
631
  message: docError.message,
644
632
  stack: docError.stack,
@@ -650,32 +638,31 @@ export const generateAndSendLabIntroduction = async (
650
638
  });
651
639
  }
652
640
  }
653
-
654
- const streamParts: StreamPart[] = finalizeDeferredToolParts(
655
- enrichStreamPartsWithGeneratedFiles(
656
- applyResolvedFormatsToStreamParts(rawStreamParts, resolvedDocs),
657
- generatedDocFiles,
658
- ),
641
+
642
+ const streamParts: StreamPart[] = enrichStreamPartsWithGeneratedFiles(
643
+ applyResolvedFormatsToStreamParts(rawStreamParts, resolvedDocs),
644
+ generatedDocFiles,
659
645
  );
660
646
 
661
- // While the message is still PENDING, resolve deferred tool indicators
662
- // (spinner → attachment or error) before the final persist event.
663
647
  for (const part of streamParts) {
664
- if (part.type !== 'tool' || part.toolName !== 'generate_document') {
665
- continue;
648
+ if (
649
+ part.type === 'tool' &&
650
+ part.toolName === 'generate_document' &&
651
+ part.fileId
652
+ ) {
653
+ await pusher
654
+ .trigger(chatChannel(conversationId), 'message-tool-call', {
655
+ messageId: emitOptions.aiMessageId,
656
+ toolCallId: part.toolCallId,
657
+ toolName: part.toolName,
658
+ ...(part.title ? { title: part.title } : {}),
659
+ ...(part.format ? { format: part.format } : {}),
660
+ fileId: part.fileId,
661
+ fileName: part.fileName,
662
+ status: 'completed',
663
+ })
664
+ .catch(() => {});
666
665
  }
667
- if (part.status === 'executing') continue;
668
- await pusher
669
- .trigger(chatChannel(conversationId), 'message-tool-call', {
670
- messageId: emitOptions.aiMessageId,
671
- toolCallId: part.toolCallId,
672
- toolName: part.toolName,
673
- ...(part.title ? { title: part.title } : {}),
674
- ...(part.format ? { format: part.format } : {}),
675
- ...(part.fileId ? { fileId: part.fileId, fileName: part.fileName } : {}),
676
- status: part.status,
677
- })
678
- .catch(() => {});
679
666
  }
680
667
 
681
668
  await completePendingAIMessage(emitOptions.aiMessageId, streamedText, {