@studious-lms/server 1.27.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 (106) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +31 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/instrument.js +9 -4
  5. package/dist/instrument.js.map +1 -1
  6. package/dist/lib/fileUpload.d.ts +15 -0
  7. package/dist/lib/fileUpload.d.ts.map +1 -1
  8. package/dist/lib/fileUpload.js +43 -2
  9. package/dist/lib/fileUpload.js.map +1 -1
  10. package/dist/pipelines/aiLabChatContract.d.ts +2 -2
  11. package/dist/pipelines/generateVisualization.d.ts +33 -0
  12. package/dist/pipelines/generateVisualization.d.ts.map +1 -0
  13. package/dist/pipelines/generateVisualization.js +79 -0
  14. package/dist/pipelines/generateVisualization.js.map +1 -0
  15. package/dist/routers/_app.d.ts +891 -53
  16. package/dist/routers/_app.d.ts.map +1 -1
  17. package/dist/routers/_app.js +4 -2
  18. package/dist/routers/_app.js.map +1 -1
  19. package/dist/routers/admin.d.ts +297 -0
  20. package/dist/routers/admin.d.ts.map +1 -1
  21. package/dist/routers/admin.js +43 -2
  22. package/dist/routers/admin.js.map +1 -1
  23. package/dist/routers/class.d.ts.map +1 -1
  24. package/dist/routers/class.js +16 -3
  25. package/dist/routers/class.js.map +1 -1
  26. package/dist/routers/module.d.ts +1 -1
  27. package/dist/routers/visualization.d.ts +124 -0
  28. package/dist/routers/visualization.d.ts.map +1 -0
  29. package/dist/routers/visualization.js +41 -0
  30. package/dist/routers/visualization.js.map +1 -0
  31. package/dist/services/adminClass.d.ts +153 -0
  32. package/dist/services/adminClass.d.ts.map +1 -0
  33. package/dist/services/adminClass.js +196 -0
  34. package/dist/services/adminClass.js.map +1 -0
  35. package/dist/services/adminInstitution.d.ts +2 -0
  36. package/dist/services/adminInstitution.d.ts.map +1 -1
  37. package/dist/services/adminInstitution.js +11 -4
  38. package/dist/services/adminInstitution.js.map +1 -1
  39. package/dist/services/adminMetrics.d.ts +8 -0
  40. package/dist/services/adminMetrics.d.ts.map +1 -1
  41. package/dist/services/adminMetrics.js +30 -3
  42. package/dist/services/adminMetrics.js.map +1 -1
  43. package/dist/services/adminSpam.d.ts +76 -0
  44. package/dist/services/adminSpam.d.ts.map +1 -0
  45. package/dist/services/adminSpam.js +163 -0
  46. package/dist/services/adminSpam.js.map +1 -0
  47. package/dist/services/adminSql.d.ts +36 -0
  48. package/dist/services/adminSql.d.ts.map +1 -0
  49. package/dist/services/adminSql.js +172 -0
  50. package/dist/services/adminSql.js.map +1 -0
  51. package/dist/services/adminTelemetry.d.ts +56 -0
  52. package/dist/services/adminTelemetry.d.ts.map +1 -0
  53. package/dist/services/adminTelemetry.js +147 -0
  54. package/dist/services/adminTelemetry.js.map +1 -0
  55. package/dist/services/adminUser.d.ts +45 -0
  56. package/dist/services/adminUser.d.ts.map +1 -1
  57. package/dist/services/adminUser.js +48 -11
  58. package/dist/services/adminUser.js.map +1 -1
  59. package/dist/services/auth.d.ts.map +1 -1
  60. package/dist/services/auth.js +10 -4
  61. package/dist/services/auth.js.map +1 -1
  62. package/dist/services/fees.d.ts.map +1 -1
  63. package/dist/services/fees.js +7 -3
  64. package/dist/services/fees.js.map +1 -1
  65. package/dist/services/module.d.ts.map +1 -1
  66. package/dist/services/module.js +23 -4
  67. package/dist/services/module.js.map +1 -1
  68. package/dist/services/visualization.d.ts +72 -0
  69. package/dist/services/visualization.d.ts.map +1 -0
  70. package/dist/services/visualization.js +122 -0
  71. package/dist/services/visualization.js.map +1 -0
  72. package/dist/utils/sentryTrpc.d.ts +3 -0
  73. package/dist/utils/sentryTrpc.d.ts.map +1 -0
  74. package/dist/utils/sentryTrpc.js +26 -0
  75. package/dist/utils/sentryTrpc.js.map +1 -0
  76. package/package.json +1 -1
  77. package/prisma/migrations/20261025120000_add_visualizations/migration.sql +27 -0
  78. package/prisma/schema.prisma +24 -0
  79. package/src/index.ts +30 -4
  80. package/src/instrument.ts +8 -2
  81. package/src/lib/fileUpload.ts +49 -0
  82. package/src/pipelines/generateVisualization.ts +107 -0
  83. package/src/routers/_app.ts +2 -0
  84. package/src/routers/admin.ts +72 -0
  85. package/src/routers/class.ts +14 -1
  86. package/src/routers/visualization.ts +60 -0
  87. package/src/services/adminClass.ts +222 -0
  88. package/src/services/adminInstitution.ts +9 -2
  89. package/src/services/adminMetrics.ts +35 -0
  90. package/src/services/adminSpam.ts +224 -0
  91. package/src/services/adminSql.ts +214 -0
  92. package/src/services/adminTelemetry.ts +194 -0
  93. package/src/services/adminUser.ts +61 -9
  94. package/src/services/auth.ts +8 -2
  95. package/src/services/fees.ts +5 -1
  96. package/src/services/module.ts +29 -2
  97. package/src/services/visualization.ts +147 -0
  98. package/src/utils/sentryTrpc.ts +25 -0
  99. package/tests/unit/lib/fileUploadSession.test.ts +67 -0
  100. package/tests/unit/services/adminClass.test.ts +102 -0
  101. package/tests/unit/services/adminSpam.test.ts +108 -0
  102. package/tests/unit/services/adminSql.test.ts +56 -0
  103. package/tests/unit/services/adminTelemetry.test.ts +47 -0
  104. package/tests/unit/services/adminUser.test.ts +2 -0
  105. package/tests/unit/services/visualization.test.ts +130 -0
  106. package/tests/unit/utils/sentryTrpc.test.ts +15 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":"AA2BA,OAAO,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"/","sources":["index.ts"],"names":[],"mappings":"AAEA,OAAO,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
+ // Must be the first import: Sentry only instruments libraries that are loaded
2
+ // after `Sentry.init()` has run.
1
3
 
2
- !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]="b7930ee6-17c9-56de-8060-9caefb2d8ea1")}catch(e){}}();
4
+ !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]="882ce804-c365-517e-a37a-1da8e8c24979")}catch(e){}}();
5
+ import "./instrument.js";
3
6
  import express from 'express';
4
7
  import { createServer } from 'http';
5
8
  import { Server } from 'socket.io';
@@ -11,16 +14,17 @@ import { findUserBySessionToken } from './models/auth.js';
11
14
  import { logger } from './utils/logger.js';
12
15
  import { setupSocketHandlers } from './socket/handlers.js';
13
16
  import { bucket } from './lib/googleCloudStorage.js';
17
+ import { validateDirectUploadSession } from './lib/fileUpload.js';
14
18
  import { prisma } from './lib/prisma.js';
15
19
  import { pusher } from './lib/pusher.js';
16
20
  import { connectRedis, disconnectRedis } from './lib/redis.js';
17
21
  import { createCorsOriginMatcher } from './lib/config/cors.js';
22
+ import { isReportableTrpcError } from './utils/sentryTrpc.js';
18
23
  import { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';
19
24
  import * as Sentry from "@sentry/node";
20
25
  import { env } from './lib/config/env.js';
21
26
  import compression from 'compression';
22
27
  import { v4 as uuidv4 } from 'uuid';
23
- import "./instrument.js";
24
28
  import { aiStreamRouter } from './routes/aiStream.js';
25
29
  const app = express();
26
30
  app.set('trust proxy', 1);
@@ -481,7 +485,7 @@ function getUploadSessionId(req) {
481
485
  return typeof headerValue === 'string' ? headerValue : headerValue?.[0];
482
486
  }
483
487
  // File upload endpoint for secure file uploads (supports both POST and PUT)
484
- app.post('/api/upload/:filePath', async (req, res) => {
488
+ app.post('/api/upload/:filePath', uploadLimiter, async (req, res) => {
485
489
  await handleFileUpload(req, res);
486
490
  });
487
491
  app.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {
@@ -499,6 +503,12 @@ async function handleFileUpload(req, res) {
499
503
  res.header('Access-Control-Allow-Origin', origin);
500
504
  }
501
505
  res.header('Access-Control-Allow-Credentials', 'true');
506
+ // Gate the write on the server-issued upload session for this exact path.
507
+ // Without this the proxy streams the body to any GCS key a caller names.
508
+ await validateDirectUploadSession({
509
+ filePath,
510
+ uploadSessionId: getUploadSessionId(req),
511
+ });
502
512
  const contentType = req.headers['content-type'] || 'application/octet-stream';
503
513
  const file = bucket.file(filePath);
504
514
  const writeStream = file.createWriteStream({
@@ -558,6 +568,23 @@ app.use('/trpc', createExpressMiddleware({
558
568
  createContext: async ({ req, res }) => {
559
569
  return createTRPCContext({ req, res });
560
570
  },
571
+ // tRPC replies with a JSON error envelope instead of throwing into Express,
572
+ // so `setupExpressErrorHandler` never sees procedure failures. Without this
573
+ // hook Sentry receives nothing for the entire API.
574
+ onError: ({ error, path, type, ctx }) => {
575
+ if (!isReportableTrpcError(error))
576
+ return;
577
+ Sentry.withScope((scope) => {
578
+ scope.setTags({
579
+ 'trpc.path': path ?? 'unknown',
580
+ 'trpc.type': type,
581
+ 'trpc.code': error.code,
582
+ });
583
+ if (ctx?.user?.id)
584
+ scope.setUser({ id: ctx.user.id });
585
+ Sentry.captureException(error.cause ?? error);
586
+ });
587
+ },
561
588
  }));
562
589
  // IMPORTANT: Sentry error handler must be added AFTER all other middleware and routes
563
590
  // but BEFORE any other error handlers
@@ -613,4 +640,4 @@ const gracefulShutdown = (signal) => {
613
640
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
614
641
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
615
642
  //# sourceMappingURL=index.js.map
616
- //# debugId=b7930ee6-17c9-56de-8060-9caefb2d8ea1
643
+ //# debugId=882ce804-c365-517e-a37a-1da8e8c24979
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["index.ts"],"sourceRoot":"/","sourcesContent":["import express from 'express';\nimport type { Request, Response } from 'express';\nimport { createServer } from 'http';\nimport { Server } from 'socket.io';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport { appRouter } from './routers/_app.js';\nimport { createTRPCContext, createCallerFactory } from './trpc.js';\nimport { findUserBySessionToken } from './models/auth.js';\nimport { logger } from './utils/logger.js';\nimport { setupSocketHandlers } from './socket/handlers.js';\nimport { bucket } from './lib/googleCloudStorage.js';\nimport { validateDirectUploadIntent } from './lib/fileUpload.js';\nimport { prisma } from './lib/prisma.js';\nimport { pusher } from './lib/pusher.js';\nimport { connectRedis, disconnectRedis } from './lib/redis.js';\nimport { createCorsOriginMatcher } from './lib/config/cors.js';\n\nimport { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';\n\nimport * as Sentry from \"@sentry/node\";\nimport { env } from './lib/config/env.js';\nimport compression from 'compression';\nimport { v4 as uuidv4 } from 'uuid';\n\n\nimport \"./instrument.js\";\nimport { openAIClient } from './utils/inference.js';\nimport { aiStreamRouter } from './routes/aiStream.js';\n\nconst app = express();\n\napp.set('trust proxy', 1);\n\napp.use(helmetConfig);\napp.use(compression());\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\n\napp.use((req, res, next) => {\n const requestId = uuidv4();\n res.setHeader('X-Request-ID', requestId);\n next();\n});\n\nconst { allowedOrigins, allowedOriginPatterns, isAllowedOrigin } = createCorsOriginMatcher(env);\n\n// CORS middleware\napp.use(cors({\n origin: (origin, callback) => {\n if (!origin || isAllowedOrigin(origin)) {\n callback(null, true);\n return;\n }\n\n logger.warn('CORS origin rejected', { origin, allowedOrigins });\n callback(null, false);\n },\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n // Omit allowedHeaders so `cors` echoes `Access-Control-Request-Headers` on preflight.\n // A fixed list breaks browsers that add headers (e.g. Sentry `baggage`, `sentry-trace`).\n preflightContinue: false,\n optionsSuccessStatus: 204,\n}));\n\napp.use(generalLimiter);\n\n// CORS debugging middleware\napp.use((req, res, next) => {\n if (req.method === 'OPTIONS' || req.path.includes('trpc')) {\n logger.info('CORS Request', {\n method: req.method,\n path: req.path,\n origin: req.headers.origin,\n userAgent: req.headers['user-agent']\n });\n }\n next();\n});\n\n// Response time logging middleware\napp.use((req, res, next) => {\n const start = Date.now();\n res.on('finish', () => {\n const duration = Date.now() - start;\n logger.info('Request completed', {\n method: req.method,\n path: req.path,\n statusCode: res.statusCode,\n duration: `${duration}ms`\n });\n });\n next();\n});\n\n// app.use(\"/panel\", async (_, res) => {\n// if (env.NODE_ENV !== \"development\") {\n// return res.status(404).send(\"Not Found\");\n// }\n\n// // Dynamically import renderTrpcPanel only in development\n// const { renderTrpcPanel } = await import(\"trpc-ui\");\n\n// return res.send(\n// renderTrpcPanel(appRouter, {\n// url: \"/trpc\", // Base url of your trpc server\n// meta: {\n// title: \"Studious Backend\",\n// description:\n// \"This is the backend for the Studious application.\",\n// },\n// })\n// );\n// });\n\n\n// Create HTTP server\nconst httpServer = createServer(app);\n\napp.get('/', async (req, res) => {\n res.status(200).json({ \n status: 'OK',\n timestamp: new Date().toISOString(),\n uptime: process.uptime(),\n version: process.env.npm_package_version,\n environment: process.env.NODE_ENV,\n appUrl: process.env.NEXT_PUBLIC_APP_URL,\n });\n});\n\napp.get('/health', async (req, res) => {\n\n try {\n // Check database connectivity\n await prisma.$queryRaw`SELECT 1`;\n \n res.status(200).json({ \n status: 'OK',\n timestamp: new Date().toISOString(),\n uptime: process.uptime(),\n database: 'connected'\n });\n } catch (error) {\n res.status(503).json({ \n status: 'ERROR',\n database: 'disconnected',\n error: error instanceof Error ? error.message : 'Unknown error'\n });\n }\n});\n\n// Pusher channel auth (for private-* and presence-* channels)\n// Token from: x-user header, or cookie (same-origin requests send cookies automatically)\napp.post('/api/pusher/auth', async (req, res) => {\n try {\n let token = req.headers['x-user'] as string | undefined;\n if (!token && req.headers.cookie) {\n const cookieName = env.PUSHER_AUTH_COOKIE_NAME || 'token';\n const match = req.headers.cookie.match(new RegExp(`${cookieName}=([^;]+)`));\n token = match?.[1]?.trim();\n }\n const { socket_id, channel_name } = req.body as { socket_id?: string; channel_name?: string };\n\n if (!socket_id || !channel_name) {\n return res.status(400).json({ error: 'socket_id and channel_name required' });\n }\n\n if (!token) {\n return res.status(401).json({ error: 'Authentication required' });\n }\n\n const user = await findUserBySessionToken(token);\n\n if (!user) {\n return res.status(401).json({ error: 'Invalid or expired session' });\n }\n\n // Verify channel access for private-conversation-* channels\n if (channel_name.startsWith('private-conversation-')) {\n const conversationId = channel_name.replace('private-conversation-', '');\n const member = await prisma.conversationMember.findFirst({\n where: { conversationId, userId: user.id },\n });\n \n if (!member) {\n return res.status(403).json({ error: 'Not a member of this conversation' });\n }\n }\n\n if (channel_name.startsWith('private-worksheet-job-')) {\n const worksheetId = channel_name.replace('private-worksheet-job-', '');\n const worksheet = await prisma.worksheet.findFirst({\n where: {\n id: worksheetId,\n class: { teachers: { some: { id: user.id } } },\n },\n });\n if (!worksheet) {\n return res.status(403).json({ error: 'No access to this worksheet job' });\n }\n } else if (channel_name.startsWith('private-worksheet-')) {\n const worksheetId = channel_name.replace('private-worksheet-', '');\n const worksheet = await prisma.studentWorksheetResponse.findFirst({\n where: { id: worksheetId, OR: [\n { studentId: user.id },\n { submission: { assignment: { class: { teachers: { some: { id: user.id } } } } } },\n ] },\n });\n if (!worksheet) {\n return res.status(403).json({ error: 'No access to this worksheet' });\n }\n }\n\n if (channel_name.startsWith('private-teacher-')) {\n const classId = channel_name.replace('private-teacher-', '');\n const isTeacher = await prisma.class.findFirst({\n where: { id: classId, teachers: { some: { id: user.id } } },\n });\n if (!isTeacher) {\n return res.status(403).json({ error: 'Not a teacher of this class' });\n }\n }\n\n if (channel_name.startsWith('private-class-') && channel_name.endsWith('-discussions')) {\n const classId = channel_name\n .replace('private-class-', '')\n .replace('-discussions', '');\n const isMember = await prisma.class.findFirst({\n where: {\n id: classId,\n discussionsEnabled: true,\n OR: [\n { students: { some: { id: user.id } } },\n { teachers: { some: { id: user.id } } },\n {\n school: {\n is: {\n memberships: {\n some: {\n userId: user.id,\n role: 'ADMIN',\n status: 'ACTIVE',\n },\n },\n },\n },\n },\n ],\n },\n });\n if (!isMember) {\n return res.status(403).json({ error: 'Not a member of this class discussion channel' });\n }\n }\n\n // Verify channel access for private-class-* channels\n // if (channel_name.startsWith('private-class-')) {\n // const classId = channel_name.replace('private-class-', '');\n // const isMember = await prisma.class.findFirst({\n // where: {\n // id: classId,\n // OR: [\n // { students: { some: { id: user.id } } },\n // { teachers: { some: { id: user.id } } },\n // ],\n // },\n // });\n // if (!isMember) {\n // return res.status(403).json({ error: 'Not a member of this class' });\n // }\n // }\n\n // Per-user notification channel: only the user themselves may subscribe\n if (channel_name.startsWith('private-user-')) {\n if (channel_name !== `private-user-${user.id}-notifications`) {\n return res.status(403).json({ error: 'No access to this user channel' });\n }\n }\n\n if (channel_name.startsWith('presence-')) {\n const authResponse = pusher.authorizeChannel(socket_id, channel_name, {\n user_id: user.id,\n user_info: { username: user.username },\n });\n return res.json(authResponse);\n }\n\n const authResponse = pusher.authorizeChannel(socket_id, channel_name);\n return res.json(authResponse);\n } catch (error) {\n logger.error('Pusher auth error', { error });\n return res.status(500).json({ error: 'Authentication failed' });\n }\n});\n\n// Setup Socket.IO\nconst io = new Server(httpServer, {\n cors: {\n origin: (origin, callback) => {\n callback(null, !origin || isAllowedOrigin(origin));\n },\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n credentials: true,\n allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Access-Control-Allow-Origin', 'x-user']\n },\n transports: ['websocket', 'polling'],\n pingTimeout: 60000,\n pingInterval: 25000,\n connectTimeout: 45000,\n path: '/socket.io/',\n allowEIO3: true\n});\n\n// Add server-level logging\nio.engine.on('connection_error', (err: Error) => {\n logger.error('Socket connection error', { error: err.message });\n});\n\n// Setup socket handlers\nsetupSocketHandlers(io);\n\n// AI streaming routes\napp.use('/api/ai', aiStreamRouter);\n\n// File serving endpoint for secure file access\napp.get('/api/files/:fileId', async (req, res) => {\n try {\n const fileId = decodeURIComponent(req.params.fileId);\n // console.log('File request:', { fileId, originalPath: req.params.fileId });\n \n // Get user from request headers\n const userHeader = req.headers['x-user'];\n if (!userHeader) {\n return res.status(401).json({ error: 'Authentication required' });\n }\n\n const token = typeof userHeader === 'string' ? userHeader : userHeader[0];\n\n // Find user by session token\n const user = await findUserBySessionToken(token);\n\n if (!user) {\n return res.status(401).json({ error: 'Invalid or expired session' });\n }\n\n // Find file in database by path\n const fileRecord = await prisma.file.findFirst({\n where: { id: fileId },\n include: {\n user: true,\n assignment: {\n include: {\n class: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n },\n submission: {\n include: {\n student: true,\n assignment: {\n include: {\n class: {\n include: {\n teachers: true\n }\n }\n }\n }\n }\n },\n annotations: {\n include: {\n student: true,\n assignment: {\n include: {\n class: {\n include: {\n teachers: true\n }\n }\n }\n }\n }\n },\n folder: {\n include: {\n class: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n },\n classDraft: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n });\n\n if (!fileRecord) {\n return res.status(404).json({ error: 'File not found in database' });\n }\n\n // Check if user has permission to access this file\n let hasPermission = false;\n\n // Check if user created the file\n if (fileRecord.userId === user.id) {\n hasPermission = true;\n }\n\n // Check if file is related to a class where user is a member\n if (!hasPermission) {\n // Check assignment files\n if (fileRecord.assignment?.class) {\n const classData = fileRecord.assignment.class;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n\n if (!hasPermission && fileRecord.annotations) {\n const annotation = fileRecord.annotations;\n if (annotation.studentId === user.id) {\n hasPermission = true;\n } else if (annotation.assignment?.class?.teachers.some(teacher => teacher.id === user.id)) {\n hasPermission = true;\n }\n }\n\n // Check submission files (student can access their own submissions, teachers can access all submissions in their class)\n if (!hasPermission && fileRecord.submission) {\n const submission = fileRecord.submission;\n if (submission.studentId === user.id) {\n hasPermission = true; // Student accessing their own submission\n } else if (submission.assignment?.class?.teachers.some(teacher => teacher.id === user.id)) {\n hasPermission = true; // Teacher accessing submission in their class\n }\n }\n\n // Check folder files\n if (!hasPermission && fileRecord.folder?.class) {\n const classData = fileRecord.folder.class;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n\n // Check class draft files\n if (!hasPermission && fileRecord.classDraft) {\n const classData = fileRecord.classDraft;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n }\n\n if (!hasPermission) {\n return res.status(403).json({ error: 'Access denied - insufficient permissions' });\n }\n \n const filePath = fileRecord.path;\n \n // Get file from Google Cloud Storage\n const file = bucket.file(filePath);\n const [exists] = await file.exists();\n \n if (!exists) {\n return res.status(404).json({ error: 'File not found in storage', filePath });\n }\n \n // Get file metadata\n const [metadata] = await file.getMetadata();\n \n // Set appropriate headers\n res.set({\n 'Content-Type': metadata.contentType || 'application/octet-stream',\n 'Content-Length': metadata.size,\n 'Cache-Control': 'public, max-age=31536000', // 1 year cache\n 'ETag': metadata.etag,\n });\n \n // Stream file to response\n const stream = file.createReadStream();\n stream.pipe(res);\n \n stream.on('error', (error) => {\n logger.error('Error streaming file:', {error});\n if (!res.headersSent) {\n res.status(500).json({ error: 'Error streaming file' });\n }\n });\n \n } catch (error) {\n logger.error('Error serving file:', {error});\n res.status(500).json({ error: 'Internal server error' });\n }\n});\n\napp.use('/trpc/auth.login', authLimiter);\napp.use('/trpc/auth.register', authLimiter);\n\nasync function getAuthenticatedRequestUser(req: Request) {\n const userHeader = req.headers['x-user'];\n const tokenFromUserHeader = typeof userHeader === 'string' ? userHeader : userHeader?.[0];\n const authorizationHeader = req.headers.authorization;\n const tokenFromAuthorization =\n typeof authorizationHeader === 'string' && authorizationHeader.startsWith('Bearer ')\n ? authorizationHeader.slice(7)\n : null;\n\n let token = tokenFromUserHeader ?? tokenFromAuthorization;\n\n if (!token && req.headers.cookie) {\n const cookieName = env.PUSHER_AUTH_COOKIE_NAME || 'token';\n const match = req.headers.cookie.match(new RegExp(cookieName + '=([^;]+)'));\n token = match?.[1]?.trim() ?? null;\n }\n\n if (!token) {\n return null;\n }\n\n return findUserBySessionToken(token);\n}\n\nfunction getUploadSessionId(req: Request) {\n const queryValue = req.query.uploadSessionId;\n if (typeof queryValue === 'string' && queryValue.trim()) {\n return queryValue;\n }\n\n const headerValue = req.headers['x-upload-session-id'];\n return typeof headerValue === 'string' ? headerValue : headerValue?.[0];\n}\n\n// File upload endpoint for secure file uploads (supports both POST and PUT)\napp.post('/api/upload/:filePath', async (req, res) => {\n await handleFileUpload(req, res);\n});\n\napp.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {\n await handleFileUpload(req, res);\n});\n\nasync function handleFileUpload(req: Request, res: Response) {\n try {\n const filePath = decodeURIComponent(req.params.filePath);\n\n // Set CORS headers for upload endpoint\n const origin = req.headers.origin;\n \n if (origin && !isAllowedOrigin(origin)) {\n return res.status(403).json({ error: 'Origin not allowed' });\n }\n\n if (origin) {\n res.header('Access-Control-Allow-Origin', origin);\n }\n \n res.header('Access-Control-Allow-Credentials', 'true');\n \n const contentType = req.headers['content-type'] || 'application/octet-stream';\n const file = bucket.file(filePath);\n\n const writeStream = file.createWriteStream({\n metadata: {\n contentType,\n },\n });\n \n writeStream.on('error', (error) => {\n logger.error('Error uploading file:', {error, filePath});\n if (!res.headersSent) {\n res.status(500).json({ error: 'Error uploading file' });\n }\n });\n \n writeStream.on('finish', () => {\n res.status(200).json({ \n success: true, \n filePath,\n message: 'File uploaded successfully' \n });\n });\n \n req.pipe(writeStream);\n \n } catch (error) {\n logger.error('Error handling file upload:', {error});\n const code = typeof error === 'object' && error && 'code' in error ? error.code : null;\n const message = error instanceof Error ? error.message : 'Internal server error';\n\n if (code === 'FORBIDDEN') {\n return res.status(403).json({ error: message });\n }\n\n if (code === 'BAD_REQUEST') {\n return res.status(400).json({ error: message });\n }\n\n res.status(500).json({ error: 'Internal server error' });\n }\n}\n\n// Create caller\nconst createCaller = createCallerFactory(appRouter);\n\n// Fallback OPTIONS handler — prevents tRPC from rejecting preflight requests\n// when the cors middleware passes through without ending the response.\napp.options('/trpc/*', (req, res) => {\n const origin = req.headers.origin;\n if (origin && isAllowedOrigin(origin)) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Credentials', 'true');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n const requestedHeaders = req.headers['access-control-request-headers'];\n if (requestedHeaders) {\n res.setHeader('Access-Control-Allow-Headers', requestedHeaders);\n res.setHeader('Vary', 'Origin, Access-Control-Request-Headers');\n }\n }\n res.sendStatus(204);\n});\n\n// Setup tRPC middleware\napp.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext: async ({ req, res }: { req: Request; res: Response }) => {\n return createTRPCContext({ req, res });\n },\n })\n);\n\n// IMPORTANT: Sentry error handler must be added AFTER all other middleware and routes\n// but BEFORE any other error handlers\nSentry.setupExpressErrorHandler(app);\n\n// app.use(function onError(err, req, res, next) {\n// // The error id is attached to `res.sentry` to be returned\n// // and optionally displayed to the user for support.\n// res.statusCode = 500;\n// res.end(res.sentry + \"\\n\");\n// });\n\n\nconst PORT = env.PORT || 3001;\n\nconnectRedis().then(() => {\n httpServer.listen(PORT, () => {\n logger.info(`Server running on port ${PORT}`, {\n port: PORT,\n services: ['tRPC', 'Socket.IO', env.REDIS_URL ? 'Redis' : null].filter(Boolean),\n });\n });\n}); \n\n// log all env variables\nlogger.info('Configurations', {\n NODE_ENV: env.NODE_ENV,\n PORT: env.PORT,\n NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,\n LOG_MODE: env.LOG_MODE,\n});\n\n// Log CORS configuration\nlogger.info('CORS Configuration', {\n allowedOrigins,\n allowedOriginPatterns: allowedOriginPatterns.map((pattern) => pattern.source),\n});\n\nconst gracefulShutdown = (signal: string) => {\n logger.info(`Received ${signal}, shutting down gracefully`);\n \n httpServer.close(() => {\n logger.info('HTTP server closed');\n \n io.close(() => {\n logger.info('Socket.IO server closed');\n\n disconnectRedis().then(() =>\n prisma.$disconnect().then(() => {\n logger.info('Database connections closed');\n process.exit(0);\n }).catch((err) => {\n logger.error('Error disconnecting from database', { error: err });\n process.exit(1);\n })\n );\n });\n });\n \n // Force shutdown after 10 seconds\n setTimeout(() => {\n logger.error('Forced shutdown after timeout');\n process.exit(1);\n }, 10000);\n};\n\nprocess.on('SIGTERM', () => gracefulShutdown('SIGTERM'));\nprocess.on('SIGINT', () => gracefulShutdown('SIGINT'));\n"],"names":[],"mappings":"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","debug_id":"b7930ee6-17c9-56de-8060-9caefb2d8ea1"}
1
+ {"version":3,"file":"index.js","sources":["index.ts"],"sourceRoot":"/","sourcesContent":["// Must be the first import: Sentry only instruments libraries that are loaded\n// after `Sentry.init()` has run.\nimport \"./instrument.js\";\n\nimport express from 'express';\nimport type { Request, Response } from 'express';\nimport { createServer } from 'http';\nimport { Server } from 'socket.io';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport { appRouter } from './routers/_app.js';\nimport { createTRPCContext, createCallerFactory } from './trpc.js';\nimport { findUserBySessionToken } from './models/auth.js';\nimport { logger } from './utils/logger.js';\nimport { setupSocketHandlers } from './socket/handlers.js';\nimport { bucket } from './lib/googleCloudStorage.js';\nimport { validateDirectUploadSession } from './lib/fileUpload.js';\nimport { prisma } from './lib/prisma.js';\nimport { pusher } from './lib/pusher.js';\nimport { connectRedis, disconnectRedis } from './lib/redis.js';\nimport { createCorsOriginMatcher } from './lib/config/cors.js';\nimport { isReportableTrpcError } from './utils/sentryTrpc.js';\n\nimport { authLimiter, generalLimiter, helmetConfig, uploadLimiter } from './middleware/security.js';\n\nimport * as Sentry from \"@sentry/node\";\nimport { env } from './lib/config/env.js';\nimport compression from 'compression';\nimport { v4 as uuidv4 } from 'uuid';\n\n\nimport { openAIClient } from './utils/inference.js';\nimport { aiStreamRouter } from './routes/aiStream.js';\n\nconst app = express();\n\napp.set('trust proxy', 1);\n\napp.use(helmetConfig);\napp.use(compression());\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\n\napp.use((req, res, next) => {\n const requestId = uuidv4();\n res.setHeader('X-Request-ID', requestId);\n next();\n});\n\nconst { allowedOrigins, allowedOriginPatterns, isAllowedOrigin } = createCorsOriginMatcher(env);\n\n// CORS middleware\napp.use(cors({\n origin: (origin, callback) => {\n if (!origin || isAllowedOrigin(origin)) {\n callback(null, true);\n return;\n }\n\n logger.warn('CORS origin rejected', { origin, allowedOrigins });\n callback(null, false);\n },\n credentials: true,\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n // Omit allowedHeaders so `cors` echoes `Access-Control-Request-Headers` on preflight.\n // A fixed list breaks browsers that add headers (e.g. Sentry `baggage`, `sentry-trace`).\n preflightContinue: false,\n optionsSuccessStatus: 204,\n}));\n\napp.use(generalLimiter);\n\n// CORS debugging middleware\napp.use((req, res, next) => {\n if (req.method === 'OPTIONS' || req.path.includes('trpc')) {\n logger.info('CORS Request', {\n method: req.method,\n path: req.path,\n origin: req.headers.origin,\n userAgent: req.headers['user-agent']\n });\n }\n next();\n});\n\n// Response time logging middleware\napp.use((req, res, next) => {\n const start = Date.now();\n res.on('finish', () => {\n const duration = Date.now() - start;\n logger.info('Request completed', {\n method: req.method,\n path: req.path,\n statusCode: res.statusCode,\n duration: `${duration}ms`\n });\n });\n next();\n});\n\n// app.use(\"/panel\", async (_, res) => {\n// if (env.NODE_ENV !== \"development\") {\n// return res.status(404).send(\"Not Found\");\n// }\n\n// // Dynamically import renderTrpcPanel only in development\n// const { renderTrpcPanel } = await import(\"trpc-ui\");\n\n// return res.send(\n// renderTrpcPanel(appRouter, {\n// url: \"/trpc\", // Base url of your trpc server\n// meta: {\n// title: \"Studious Backend\",\n// description:\n// \"This is the backend for the Studious application.\",\n// },\n// })\n// );\n// });\n\n\n// Create HTTP server\nconst httpServer = createServer(app);\n\napp.get('/', async (req, res) => {\n res.status(200).json({ \n status: 'OK',\n timestamp: new Date().toISOString(),\n uptime: process.uptime(),\n version: process.env.npm_package_version,\n environment: process.env.NODE_ENV,\n appUrl: process.env.NEXT_PUBLIC_APP_URL,\n });\n});\n\napp.get('/health', async (req, res) => {\n\n try {\n // Check database connectivity\n await prisma.$queryRaw`SELECT 1`;\n \n res.status(200).json({ \n status: 'OK',\n timestamp: new Date().toISOString(),\n uptime: process.uptime(),\n database: 'connected'\n });\n } catch (error) {\n res.status(503).json({ \n status: 'ERROR',\n database: 'disconnected',\n error: error instanceof Error ? error.message : 'Unknown error'\n });\n }\n});\n\n// Pusher channel auth (for private-* and presence-* channels)\n// Token from: x-user header, or cookie (same-origin requests send cookies automatically)\napp.post('/api/pusher/auth', async (req, res) => {\n try {\n let token = req.headers['x-user'] as string | undefined;\n if (!token && req.headers.cookie) {\n const cookieName = env.PUSHER_AUTH_COOKIE_NAME || 'token';\n const match = req.headers.cookie.match(new RegExp(`${cookieName}=([^;]+)`));\n token = match?.[1]?.trim();\n }\n const { socket_id, channel_name } = req.body as { socket_id?: string; channel_name?: string };\n\n if (!socket_id || !channel_name) {\n return res.status(400).json({ error: 'socket_id and channel_name required' });\n }\n\n if (!token) {\n return res.status(401).json({ error: 'Authentication required' });\n }\n\n const user = await findUserBySessionToken(token);\n\n if (!user) {\n return res.status(401).json({ error: 'Invalid or expired session' });\n }\n\n // Verify channel access for private-conversation-* channels\n if (channel_name.startsWith('private-conversation-')) {\n const conversationId = channel_name.replace('private-conversation-', '');\n const member = await prisma.conversationMember.findFirst({\n where: { conversationId, userId: user.id },\n });\n \n if (!member) {\n return res.status(403).json({ error: 'Not a member of this conversation' });\n }\n }\n\n if (channel_name.startsWith('private-worksheet-job-')) {\n const worksheetId = channel_name.replace('private-worksheet-job-', '');\n const worksheet = await prisma.worksheet.findFirst({\n where: {\n id: worksheetId,\n class: { teachers: { some: { id: user.id } } },\n },\n });\n if (!worksheet) {\n return res.status(403).json({ error: 'No access to this worksheet job' });\n }\n } else if (channel_name.startsWith('private-worksheet-')) {\n const worksheetId = channel_name.replace('private-worksheet-', '');\n const worksheet = await prisma.studentWorksheetResponse.findFirst({\n where: { id: worksheetId, OR: [\n { studentId: user.id },\n { submission: { assignment: { class: { teachers: { some: { id: user.id } } } } } },\n ] },\n });\n if (!worksheet) {\n return res.status(403).json({ error: 'No access to this worksheet' });\n }\n }\n\n if (channel_name.startsWith('private-teacher-')) {\n const classId = channel_name.replace('private-teacher-', '');\n const isTeacher = await prisma.class.findFirst({\n where: { id: classId, teachers: { some: { id: user.id } } },\n });\n if (!isTeacher) {\n return res.status(403).json({ error: 'Not a teacher of this class' });\n }\n }\n\n if (channel_name.startsWith('private-class-') && channel_name.endsWith('-discussions')) {\n const classId = channel_name\n .replace('private-class-', '')\n .replace('-discussions', '');\n const isMember = await prisma.class.findFirst({\n where: {\n id: classId,\n discussionsEnabled: true,\n OR: [\n { students: { some: { id: user.id } } },\n { teachers: { some: { id: user.id } } },\n {\n school: {\n is: {\n memberships: {\n some: {\n userId: user.id,\n role: 'ADMIN',\n status: 'ACTIVE',\n },\n },\n },\n },\n },\n ],\n },\n });\n if (!isMember) {\n return res.status(403).json({ error: 'Not a member of this class discussion channel' });\n }\n }\n\n // Verify channel access for private-class-* channels\n // if (channel_name.startsWith('private-class-')) {\n // const classId = channel_name.replace('private-class-', '');\n // const isMember = await prisma.class.findFirst({\n // where: {\n // id: classId,\n // OR: [\n // { students: { some: { id: user.id } } },\n // { teachers: { some: { id: user.id } } },\n // ],\n // },\n // });\n // if (!isMember) {\n // return res.status(403).json({ error: 'Not a member of this class' });\n // }\n // }\n\n // Per-user notification channel: only the user themselves may subscribe\n if (channel_name.startsWith('private-user-')) {\n if (channel_name !== `private-user-${user.id}-notifications`) {\n return res.status(403).json({ error: 'No access to this user channel' });\n }\n }\n\n if (channel_name.startsWith('presence-')) {\n const authResponse = pusher.authorizeChannel(socket_id, channel_name, {\n user_id: user.id,\n user_info: { username: user.username },\n });\n return res.json(authResponse);\n }\n\n const authResponse = pusher.authorizeChannel(socket_id, channel_name);\n return res.json(authResponse);\n } catch (error) {\n logger.error('Pusher auth error', { error });\n return res.status(500).json({ error: 'Authentication failed' });\n }\n});\n\n// Setup Socket.IO\nconst io = new Server(httpServer, {\n cors: {\n origin: (origin, callback) => {\n callback(null, !origin || isAllowedOrigin(origin));\n },\n methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],\n credentials: true,\n allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Access-Control-Allow-Origin', 'x-user']\n },\n transports: ['websocket', 'polling'],\n pingTimeout: 60000,\n pingInterval: 25000,\n connectTimeout: 45000,\n path: '/socket.io/',\n allowEIO3: true\n});\n\n// Add server-level logging\nio.engine.on('connection_error', (err: Error) => {\n logger.error('Socket connection error', { error: err.message });\n});\n\n// Setup socket handlers\nsetupSocketHandlers(io);\n\n// AI streaming routes\napp.use('/api/ai', aiStreamRouter);\n\n// File serving endpoint for secure file access\napp.get('/api/files/:fileId', async (req, res) => {\n try {\n const fileId = decodeURIComponent(req.params.fileId);\n // console.log('File request:', { fileId, originalPath: req.params.fileId });\n \n // Get user from request headers\n const userHeader = req.headers['x-user'];\n if (!userHeader) {\n return res.status(401).json({ error: 'Authentication required' });\n }\n\n const token = typeof userHeader === 'string' ? userHeader : userHeader[0];\n\n // Find user by session token\n const user = await findUserBySessionToken(token);\n\n if (!user) {\n return res.status(401).json({ error: 'Invalid or expired session' });\n }\n\n // Find file in database by path\n const fileRecord = await prisma.file.findFirst({\n where: { id: fileId },\n include: {\n user: true,\n assignment: {\n include: {\n class: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n },\n submission: {\n include: {\n student: true,\n assignment: {\n include: {\n class: {\n include: {\n teachers: true\n }\n }\n }\n }\n }\n },\n annotations: {\n include: {\n student: true,\n assignment: {\n include: {\n class: {\n include: {\n teachers: true\n }\n }\n }\n }\n }\n },\n folder: {\n include: {\n class: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n },\n classDraft: {\n include: {\n students: true,\n teachers: true\n }\n }\n }\n });\n\n if (!fileRecord) {\n return res.status(404).json({ error: 'File not found in database' });\n }\n\n // Check if user has permission to access this file\n let hasPermission = false;\n\n // Check if user created the file\n if (fileRecord.userId === user.id) {\n hasPermission = true;\n }\n\n // Check if file is related to a class where user is a member\n if (!hasPermission) {\n // Check assignment files\n if (fileRecord.assignment?.class) {\n const classData = fileRecord.assignment.class;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n\n if (!hasPermission && fileRecord.annotations) {\n const annotation = fileRecord.annotations;\n if (annotation.studentId === user.id) {\n hasPermission = true;\n } else if (annotation.assignment?.class?.teachers.some(teacher => teacher.id === user.id)) {\n hasPermission = true;\n }\n }\n\n // Check submission files (student can access their own submissions, teachers can access all submissions in their class)\n if (!hasPermission && fileRecord.submission) {\n const submission = fileRecord.submission;\n if (submission.studentId === user.id) {\n hasPermission = true; // Student accessing their own submission\n } else if (submission.assignment?.class?.teachers.some(teacher => teacher.id === user.id)) {\n hasPermission = true; // Teacher accessing submission in their class\n }\n }\n\n // Check folder files\n if (!hasPermission && fileRecord.folder?.class) {\n const classData = fileRecord.folder.class;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n\n // Check class draft files\n if (!hasPermission && fileRecord.classDraft) {\n const classData = fileRecord.classDraft;\n const isStudent = classData.students.some(student => student.id === user.id);\n const isTeacher = classData.teachers.some(teacher => teacher.id === user.id);\n if (isStudent || isTeacher) {\n hasPermission = true;\n }\n }\n }\n\n if (!hasPermission) {\n return res.status(403).json({ error: 'Access denied - insufficient permissions' });\n }\n \n const filePath = fileRecord.path;\n \n // Get file from Google Cloud Storage\n const file = bucket.file(filePath);\n const [exists] = await file.exists();\n \n if (!exists) {\n return res.status(404).json({ error: 'File not found in storage', filePath });\n }\n \n // Get file metadata\n const [metadata] = await file.getMetadata();\n \n // Set appropriate headers\n res.set({\n 'Content-Type': metadata.contentType || 'application/octet-stream',\n 'Content-Length': metadata.size,\n 'Cache-Control': 'public, max-age=31536000', // 1 year cache\n 'ETag': metadata.etag,\n });\n \n // Stream file to response\n const stream = file.createReadStream();\n stream.pipe(res);\n \n stream.on('error', (error) => {\n logger.error('Error streaming file:', {error});\n if (!res.headersSent) {\n res.status(500).json({ error: 'Error streaming file' });\n }\n });\n \n } catch (error) {\n logger.error('Error serving file:', {error});\n res.status(500).json({ error: 'Internal server error' });\n }\n});\n\napp.use('/trpc/auth.login', authLimiter);\napp.use('/trpc/auth.register', authLimiter);\n\nasync function getAuthenticatedRequestUser(req: Request) {\n const userHeader = req.headers['x-user'];\n const tokenFromUserHeader = typeof userHeader === 'string' ? userHeader : userHeader?.[0];\n const authorizationHeader = req.headers.authorization;\n const tokenFromAuthorization =\n typeof authorizationHeader === 'string' && authorizationHeader.startsWith('Bearer ')\n ? authorizationHeader.slice(7)\n : null;\n\n let token = tokenFromUserHeader ?? tokenFromAuthorization;\n\n if (!token && req.headers.cookie) {\n const cookieName = env.PUSHER_AUTH_COOKIE_NAME || 'token';\n const match = req.headers.cookie.match(new RegExp(cookieName + '=([^;]+)'));\n token = match?.[1]?.trim() ?? null;\n }\n\n if (!token) {\n return null;\n }\n\n return findUserBySessionToken(token);\n}\n\nfunction getUploadSessionId(req: Request) {\n const queryValue = req.query.uploadSessionId;\n if (typeof queryValue === 'string' && queryValue.trim()) {\n return queryValue;\n }\n\n const headerValue = req.headers['x-upload-session-id'];\n return typeof headerValue === 'string' ? headerValue : headerValue?.[0];\n}\n\n// File upload endpoint for secure file uploads (supports both POST and PUT)\napp.post('/api/upload/:filePath', uploadLimiter, async (req, res) => {\n await handleFileUpload(req, res);\n});\n\napp.put('/api/upload/:filePath', uploadLimiter, async (req, res) => {\n await handleFileUpload(req, res);\n});\n\nasync function handleFileUpload(req: Request, res: Response) {\n try {\n const filePath = decodeURIComponent(req.params.filePath);\n\n // Set CORS headers for upload endpoint\n const origin = req.headers.origin;\n \n if (origin && !isAllowedOrigin(origin)) {\n return res.status(403).json({ error: 'Origin not allowed' });\n }\n\n if (origin) {\n res.header('Access-Control-Allow-Origin', origin);\n }\n \n res.header('Access-Control-Allow-Credentials', 'true');\n\n // Gate the write on the server-issued upload session for this exact path.\n // Without this the proxy streams the body to any GCS key a caller names.\n await validateDirectUploadSession({\n filePath,\n uploadSessionId: getUploadSessionId(req),\n });\n\n const contentType = req.headers['content-type'] || 'application/octet-stream';\n const file = bucket.file(filePath);\n\n const writeStream = file.createWriteStream({\n metadata: {\n contentType,\n },\n });\n \n writeStream.on('error', (error) => {\n logger.error('Error uploading file:', {error, filePath});\n if (!res.headersSent) {\n res.status(500).json({ error: 'Error uploading file' });\n }\n });\n \n writeStream.on('finish', () => {\n res.status(200).json({ \n success: true, \n filePath,\n message: 'File uploaded successfully' \n });\n });\n \n req.pipe(writeStream);\n \n } catch (error) {\n logger.error('Error handling file upload:', {error});\n const code = typeof error === 'object' && error && 'code' in error ? error.code : null;\n const message = error instanceof Error ? error.message : 'Internal server error';\n\n if (code === 'FORBIDDEN') {\n return res.status(403).json({ error: message });\n }\n\n if (code === 'BAD_REQUEST') {\n return res.status(400).json({ error: message });\n }\n\n res.status(500).json({ error: 'Internal server error' });\n }\n}\n\n// Create caller\nconst createCaller = createCallerFactory(appRouter);\n\n// Fallback OPTIONS handler — prevents tRPC from rejecting preflight requests\n// when the cors middleware passes through without ending the response.\napp.options('/trpc/*', (req, res) => {\n const origin = req.headers.origin;\n if (origin && isAllowedOrigin(origin)) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Credentials', 'true');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n const requestedHeaders = req.headers['access-control-request-headers'];\n if (requestedHeaders) {\n res.setHeader('Access-Control-Allow-Headers', requestedHeaders);\n res.setHeader('Vary', 'Origin, Access-Control-Request-Headers');\n }\n }\n res.sendStatus(204);\n});\n\n// Setup tRPC middleware\napp.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext: async ({ req, res }: { req: Request; res: Response }) => {\n return createTRPCContext({ req, res });\n },\n // tRPC replies with a JSON error envelope instead of throwing into Express,\n // so `setupExpressErrorHandler` never sees procedure failures. Without this\n // hook Sentry receives nothing for the entire API.\n onError: ({ error, path, type, ctx }) => {\n if (!isReportableTrpcError(error)) return;\n Sentry.withScope((scope) => {\n scope.setTags({\n 'trpc.path': path ?? 'unknown',\n 'trpc.type': type,\n 'trpc.code': error.code,\n });\n if (ctx?.user?.id) scope.setUser({ id: ctx.user.id });\n Sentry.captureException(error.cause ?? error);\n });\n },\n })\n);\n\n// IMPORTANT: Sentry error handler must be added AFTER all other middleware and routes\n// but BEFORE any other error handlers\nSentry.setupExpressErrorHandler(app);\n\n// app.use(function onError(err, req, res, next) {\n// // The error id is attached to `res.sentry` to be returned\n// // and optionally displayed to the user for support.\n// res.statusCode = 500;\n// res.end(res.sentry + \"\\n\");\n// });\n\n\nconst PORT = env.PORT || 3001;\n\nconnectRedis().then(() => {\n httpServer.listen(PORT, () => {\n logger.info(`Server running on port ${PORT}`, {\n port: PORT,\n services: ['tRPC', 'Socket.IO', env.REDIS_URL ? 'Redis' : null].filter(Boolean),\n });\n });\n}); \n\n// log all env variables\nlogger.info('Configurations', {\n NODE_ENV: env.NODE_ENV,\n PORT: env.PORT,\n NEXT_PUBLIC_APP_URL: env.NEXT_PUBLIC_APP_URL,\n LOG_MODE: env.LOG_MODE,\n});\n\n// Log CORS configuration\nlogger.info('CORS Configuration', {\n allowedOrigins,\n allowedOriginPatterns: allowedOriginPatterns.map((pattern) => pattern.source),\n});\n\nconst gracefulShutdown = (signal: string) => {\n logger.info(`Received ${signal}, shutting down gracefully`);\n \n httpServer.close(() => {\n logger.info('HTTP server closed');\n \n io.close(() => {\n logger.info('Socket.IO server closed');\n\n disconnectRedis().then(() =>\n prisma.$disconnect().then(() => {\n logger.info('Database connections closed');\n process.exit(0);\n }).catch((err) => {\n logger.error('Error disconnecting from database', { error: err });\n process.exit(1);\n })\n );\n });\n });\n \n // Force shutdown after 10 seconds\n setTimeout(() => {\n logger.error('Forced shutdown after timeout');\n process.exit(1);\n }, 10000);\n};\n\nprocess.on('SIGTERM', () => gracefulShutdown('SIGTERM'));\nprocess.on('SIGINT', () => gracefulShutdown('SIGINT'));\n"],"names":[],"mappings":"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","debug_id":"882ce804-c365-517e-a37a-1da8e8c24979"}
@@ -1,18 +1,23 @@
1
1
 
2
- !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]="5806dd47-56ef-5a55-a383-1c966636efbf")}catch(e){}}();
2
+ !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]="430fcde9-49a0-51c7-9da7-830450dc25f0")}catch(e){}}();
3
3
  import * as Sentry from "@sentry/node";
4
4
  import { env } from "./lib/config/env.js";
5
+ import { logger } from "./utils/logger.js";
5
6
  // Only initialize Sentry in non-test environments
6
7
  if (env.NODE_ENV !== 'test') {
8
+ if (!env.SENTRY_DSN) {
9
+ // Without a DSN the SDK silently drops everything, which looks identical
10
+ // to "no errors happened" — say so once at boot instead.
11
+ logger.warn('SENTRY_DSN is not set: error reporting is disabled');
12
+ }
7
13
  Sentry.init({
8
14
  dsn: env.SENTRY_DSN,
9
15
  environment: env.NODE_ENV || 'development',
10
16
  // Setting this option to true will send default PII data to Sentry.
11
17
  // For example, automatic IP address collection on events
12
18
  sendDefaultPii: false,
13
- // @todo: disable in test environment
14
- enabled: true, // Explicitly disable in test environment
19
+ enabled: Boolean(env.SENTRY_DSN),
15
20
  });
16
21
  }
17
22
  //# sourceMappingURL=instrument.js.map
18
- //# debugId=5806dd47-56ef-5a55-a383-1c966636efbf
23
+ //# debugId=430fcde9-49a0-51c7-9da7-830450dc25f0
@@ -1 +1 @@
1
- {"version":3,"file":"instrument.js","sources":["instrument.ts"],"sourceRoot":"/","sourcesContent":["import * as Sentry from \"@sentry/node\";\nimport { env } from \"./lib/config/env.js\";\n\n// Only initialize Sentry in non-test environments\nif (env.NODE_ENV !== 'test') {\n Sentry.init({\n dsn: env.SENTRY_DSN,\n environment: env.NODE_ENV || 'development',\n // Setting this option to true will send default PII data to Sentry.\n // For example, automatic IP address collection on events\n sendDefaultPii: false,\n // @todo: disable in test environment\n enabled: true, // Explicitly disable in test environment\n });\n}\n"],"names":[],"mappings":";;AAAA,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAE1C,kDAAkD;AAClD,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;IAC5B,MAAM,CAAC,IAAI,CAAC;QACV,GAAG,EAAE,GAAG,CAAC,UAAU;QACnB,WAAW,EAAE,GAAG,CAAC,QAAQ,IAAI,aAAa;QAC1C,oEAAoE;QACpE,yDAAyD;QACzD,cAAc,EAAE,KAAK;QACrB,qCAAqC;QACrC,OAAO,EAAE,IAAI,EAAE,yCAAyC;KACzD,CAAC,CAAC;AACL,CAAC","debug_id":"5806dd47-56ef-5a55-a383-1c966636efbf"}
1
+ {"version":3,"file":"instrument.js","sources":["instrument.ts"],"sourceRoot":"/","sourcesContent":["import * as Sentry from \"@sentry/node\";\nimport { env } from \"./lib/config/env.js\";\nimport { logger } from \"./utils/logger.js\";\n\n// Only initialize Sentry in non-test environments\nif (env.NODE_ENV !== 'test') {\n if (!env.SENTRY_DSN) {\n // Without a DSN the SDK silently drops everything, which looks identical\n // to \"no errors happened\" — say so once at boot instead.\n logger.warn('SENTRY_DSN is not set: error reporting is disabled');\n }\n\n Sentry.init({\n dsn: env.SENTRY_DSN,\n environment: env.NODE_ENV || 'development',\n // Setting this option to true will send default PII data to Sentry.\n // For example, automatic IP address collection on events\n sendDefaultPii: false,\n enabled: Boolean(env.SENTRY_DSN),\n });\n}\n"],"names":[],"mappings":";;AAAA,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,kDAAkD;AAClD,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;IAC5B,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;QACpB,yEAAyE;QACzE,yDAAyD;QACzD,MAAM,CAAC,IAAI,CAAC,oDAAoD,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,CAAC,IAAI,CAAC;QACV,GAAG,EAAE,GAAG,CAAC,UAAU;QACnB,WAAW,EAAE,GAAG,CAAC,QAAQ,IAAI,aAAa;QAC1C,oEAAoE;QACpE,yDAAyD;QACzD,cAAc,EAAE,KAAK;QACrB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;KACjC,CAAC,CAAC;AACL,CAAC","debug_id":"430fcde9-49a0-51c7-9da7-830450dc25f0"}
@@ -34,6 +34,21 @@ export declare function validateDirectUploadIntent(input: {
34
34
  id: string;
35
35
  uploadStatus: import(".prisma/client").$Enums.UploadStatus;
36
36
  }>;
37
+ /**
38
+ * Capability check for the `/api/upload/:filePath` proxy. The browser uploads
39
+ * with no auth header (only the server-issued `uploadSessionId` in the URL),
40
+ * so the session id + path pair *is* the capability: it maps to exactly one
41
+ * pending, unexpired file record that the server created for the requesting
42
+ * owner. Without this, the proxy would write the request body to any GCS
43
+ * object path a caller names.
44
+ */
45
+ export declare function validateDirectUploadSession(input: {
46
+ filePath: string;
47
+ uploadSessionId: string | undefined;
48
+ }): Promise<{
49
+ id: string;
50
+ uploadStatus: import(".prisma/client").$Enums.UploadStatus;
51
+ }>;
37
52
  /**
38
53
  * @deprecated Use createDirectUploadFile instead
39
54
  */
@@ -1 +1 @@
1
- {"version":3,"file":"fileUpload.d.ts","sourceRoot":"/","sources":["lib/fileUpload.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,0BAA0B,CAAC,KAAK,EAAE;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;;;GA+BA;AAKD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,EACrB,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAkG3B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,OAAO,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,cAAc,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,EACrB,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
1
+ {"version":3,"file":"fileUpload.d.ts","sourceRoot":"/","sources":["lib/fileUpload.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAEd;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,IAAI,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,0BAA0B,CAAC,KAAK,EAAE;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;;;GA+BA;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;;;GAoCA;AAKD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,KAAK,EAAE,QAAQ,EAAE,EACjB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAKzB;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlE;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,EACrB,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAkG3B;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,OAAO,EACtB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Df;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,cAAc,EAAE,EACvB,MAAM,EAAE,MAAM,EACd,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,MAAM,EACrB,cAAc,CAAC,EAAE,MAAM,EACvB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyB7B"}
@@ -1,5 +1,5 @@
1
1
 
2
- !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]="c2b8462a-2a3e-5192-85cc-5f8cc649bfec")}catch(e){}}();
2
+ !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]="9afb6b2d-d7ed-55cd-b152-c73f70b17137")}catch(e){}}();
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
@@ -35,6 +35,47 @@ export async function validateDirectUploadIntent(input) {
35
35
  }
36
36
  return fileRecord;
37
37
  }
38
+ /**
39
+ * Capability check for the `/api/upload/:filePath` proxy. The browser uploads
40
+ * with no auth header (only the server-issued `uploadSessionId` in the URL),
41
+ * so the session id + path pair *is* the capability: it maps to exactly one
42
+ * pending, unexpired file record that the server created for the requesting
43
+ * owner. Without this, the proxy would write the request body to any GCS
44
+ * object path a caller names.
45
+ */
46
+ export async function validateDirectUploadSession(input) {
47
+ if (!input.uploadSessionId) {
48
+ throw new TRPCError({
49
+ code: 'FORBIDDEN',
50
+ message: 'Missing upload session',
51
+ });
52
+ }
53
+ const fileRecord = await prisma.file.findFirst({
54
+ where: {
55
+ path: input.filePath,
56
+ uploadSessionId: input.uploadSessionId,
57
+ uploadExpiresAt: { gt: new Date() },
58
+ isOrphaned: false,
59
+ },
60
+ select: {
61
+ id: true,
62
+ uploadStatus: true,
63
+ },
64
+ });
65
+ if (!fileRecord) {
66
+ throw new TRPCError({
67
+ code: 'FORBIDDEN',
68
+ message: 'Invalid or expired upload intent',
69
+ });
70
+ }
71
+ if (!['PENDING', 'UPLOADING'].includes(fileRecord.uploadStatus)) {
72
+ throw new TRPCError({
73
+ code: 'BAD_REQUEST',
74
+ message: 'Upload is no longer accepting data',
75
+ });
76
+ }
77
+ return fileRecord;
78
+ }
38
79
  // DEPRECATED: These functions are no longer used - files are uploaded directly to GCS
39
80
  // Use createDirectUploadFile() and createDirectUploadFiles() instead
40
81
  /**
@@ -301,4 +342,4 @@ export async function createDirectUploadFiles(files, userId, directory, assignme
301
342
  }
302
343
  }
303
344
  //# sourceMappingURL=fileUpload.js.map
304
- //# debugId=c2b8462a-2a3e-5192-85cc-5f8cc649bfec
345
+ //# debugId=9afb6b2d-d7ed-55cd-b152-c73f70b17137
@@ -1 +1 @@
1
- {"version":3,"file":"fileUpload.js","sources":["lib/fileUpload.ts"],"sourceRoot":"/","sourcesContent":["import { TRPCError } from \"@trpc/server\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { getSignedUrl, objectExists } from \"./googleCloudStorage.js\";\nimport { prisma } from \"./prisma.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { env } from \"./config/env.js\";\n\nexport interface FileData {\n name: string;\n type: string;\n size: number;\n // No data field - for direct file uploads\n}\n\nexport interface DirectFileData {\n name: string;\n type: string;\n size: number;\n // No data field - for direct file uploads\n}\n\nexport interface UploadedFile {\n id: string;\n name: string;\n type: string;\n size: number;\n path: string;\n thumbnailId?: string;\n}\n\nexport interface DirectUploadFile {\n id: string;\n name: string;\n type: string;\n size: number;\n path: string;\n uploadUrl: string;\n uploadExpiresAt: Date;\n uploadSessionId: string;\n}\n\nexport async function validateDirectUploadIntent(input: {\n filePath: string;\n uploadSessionId: string;\n userId: string;\n}) {\n const fileRecord = await prisma.file.findFirst({\n where: {\n path: input.filePath,\n uploadSessionId: input.uploadSessionId,\n userId: input.userId,\n uploadExpiresAt: { gt: new Date() },\n isOrphaned: false,\n messageId: null,\n },\n select: {\n id: true,\n uploadStatus: true,\n },\n });\n\n if (!fileRecord) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Invalid or expired upload intent',\n });\n }\n\n if (!['PENDING', 'UPLOADING'].includes(fileRecord.uploadStatus)) {\n throw new TRPCError({\n code: 'BAD_REQUEST',\n message: 'Upload is no longer accepting data',\n });\n }\n\n return fileRecord;\n}\n\n// DEPRECATED: These functions are no longer used - files are uploaded directly to GCS\n// Use createDirectUploadFile() and createDirectUploadFiles() instead\n\n/**\n * @deprecated Use createDirectUploadFile instead\n */\nexport async function uploadFile(\n file: FileData,\n userId: string,\n directory?: string,\n assignmentId?: string\n): Promise<UploadedFile> {\n throw new TRPCError({\n code: 'NOT_IMPLEMENTED',\n message: 'uploadFile is deprecated. Use createDirectUploadFile instead.',\n });\n}\n\n/**\n * @deprecated Use createDirectUploadFiles instead\n */\nexport async function uploadFiles(\n files: FileData[], \n userId: string,\n directory?: string\n): Promise<UploadedFile[]> {\n throw new TRPCError({\n code: 'NOT_IMPLEMENTED',\n message: 'uploadFiles is deprecated. Use createDirectUploadFiles instead.',\n });\n}\n\n/**\n * Gets a signed URL for a file\n * @param filePath The path of the file in Google Cloud Storage\n * @returns The signed URL\n */\nexport async function getFileUrl(filePath: string): Promise<string> {\n try {\n return await getSignedUrl(filePath);\n } catch (error) {\n console.error('Error getting signed URL:', error);\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to get file URL',\n });\n }\n}\n\n/**\n * Creates a file record for direct upload and generates signed URL\n * @param file The file metadata (no base64 data)\n * @param userId The ID of the user uploading the file\n * @param directory Optional directory to store the file in\n * @param assignmentId Optional assignment ID to associate the file with\n * @param submissionId Optional submission ID to associate the file with\n * @returns The direct upload file information with signed URL\n */\nexport async function createDirectUploadFile(\n file: DirectFileData,\n userId: string,\n directory?: string,\n assignmentId?: string,\n submissionId?: string,\n announcementId?: string,\n conversationId?: string\n): Promise<DirectUploadFile> {\n try {\n // Validate file extension matches MIME type\n const fileExtension = file.name.split('.').pop()?.toLowerCase();\n const mimeType = file.type.toLowerCase();\n \n const extensionMimeMap: Record<string, string[]> = {\n 'jpg': ['image/jpeg'],\n 'jpeg': ['image/jpeg'],\n 'png': ['image/png'],\n 'gif': ['image/gif'],\n 'webp': ['image/webp']\n };\n \n if (fileExtension && extensionMimeMap[fileExtension]) {\n if (!extensionMimeMap[fileExtension].includes(mimeType)) {\n throw new Error(`File extension .${fileExtension} does not match MIME type ${mimeType}`);\n }\n }\n \n // Create a unique filename\n const uniqueFilename = `${uuidv4()}.${fileExtension}`;\n \n // Construct the full path\n const filePath = directory \n ? `${directory}/${uniqueFilename}`\n : uniqueFilename;\n \n // Generate upload session ID\n const uploadSessionId = uuidv4();\n \n // Generate backend proxy upload URL (not direct GCS)\n const baseUrl = env.BACKEND_URL || 'http://localhost:3001';\n const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}?uploadSessionId=${encodeURIComponent(uploadSessionId)}`;\n const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now\n \n // Create file record in database with PENDING status\n const fileRecord = await prisma.file.create({\n data: {\n name: file.name,\n type: file.type,\n size: file.size,\n path: filePath,\n uploadStatus: 'PENDING',\n uploadUrl,\n uploadExpiresAt,\n uploadSessionId,\n uploadedAt: null,\n user: {\n connect: { id: userId }\n },\n ...(directory && {\n folder: {\n connect: {id: directory},\n },\n }),\n ...(assignmentId && {\n assignment: {\n connect: { id: assignmentId }\n }\n }),\n ...(submissionId && {\n submission: {\n connect: { id: submissionId }\n }\n }),\n ...(announcementId && {\n announcement: {\n connect: { id: announcementId }\n }\n }),\n ...(conversationId && {\n conversationId,\n })\n },\n });\n \n return {\n id: fileRecord.id,\n name: file.name,\n type: file.type,\n size: file.size,\n path: filePath,\n uploadUrl,\n uploadExpiresAt,\n uploadSessionId\n };\n } catch (error) {\n logger.error('Error creating direct upload file:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to create direct upload file',\n });\n }\n}\n\n/**\n * Confirms a direct upload was successful\n * @param fileId The ID of the file record\n * @param uploadSuccess Whether the upload was successful\n * @param errorMessage Optional error message if upload failed\n */\nexport async function confirmDirectUpload(\n fileId: string,\n uploadSuccess: boolean,\n errorMessage?: string\n): Promise<void> {\n try {\n // First fetch the file record to get the object path\n const fileRecord = await prisma.file.findUnique({\n where: { id: fileId },\n select: { path: true }\n });\n\n if (!fileRecord) {\n throw new TRPCError({\n code: 'NOT_FOUND',\n message: 'File record not found',\n });\n }\n\n let actualUploadSuccess = uploadSuccess;\n let actualErrorMessage = errorMessage;\n\n // If uploadSuccess is true, verify the object actually exists in GCS\n if (uploadSuccess) {\n try {\n const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);\n if (!exists) {\n actualUploadSuccess = false;\n actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';\n logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);\n }\n } catch (error) {\n logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n actualUploadSuccess = false;\n actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';\n }\n }\n\n const updateData: any = {\n uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',\n uploadProgress: actualUploadSuccess ? 100 : 0,\n uploadedAt: actualUploadSuccess ? new Date() : null,\n };\n \n if (!actualUploadSuccess && actualErrorMessage) {\n updateData.uploadError = actualErrorMessage;\n updateData.uploadRetryCount = { increment: 1 };\n }\n \n await prisma.file.update({\n where: { id: fileId },\n data: updateData\n });\n } catch (error) {\n logger.error('Error confirming direct upload:', {error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to confirm upload',\n });\n }\n}\n\n/**\n * Updates upload progress for a direct upload\n * @param fileId The ID of the file record\n * @param progress Progress percentage (0-100)\n */\nexport async function updateUploadProgress(\n fileId: string,\n progress: number\n): Promise<void> {\n try {\n // await prisma.file.update({\n // where: { id: fileId },\n // data: {\n // uploadStatus: 'UPLOADING',\n // uploadProgress: Math.min(100, Math.max(0, progress))\n // }\n // });\n const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });\n if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;\n const clamped = Math.min(100, Math.max(0, progress));\n await prisma.file.update({\n where: { id: fileId },\n data: {\n uploadStatus: 'UPLOADING',\n uploadProgress: clamped\n }\n });\n } catch (error) {\n logger.error('Error updating upload progress:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error}); \n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to update upload progress',\n });\n }\n}\n\n/**\n * Creates multiple direct upload files\n * @param files Array of file metadata\n * @param userId The ID of the user uploading the files\n * @param directory Optional subdirectory to store the files in\n * @param assignmentId Optional assignment ID to associate files with\n * @param submissionId Optional submission ID to associate files with\n * @returns Array of direct upload file information\n */\nexport async function createDirectUploadFiles(\n files: DirectFileData[], \n userId: string,\n directory?: string,\n assignmentId?: string,\n submissionId?: string,\n announcementId?: string,\n conversationId?: string\n): Promise<DirectUploadFile[]> {\n try {\n const uploadPromises = files.map(file => \n createDirectUploadFile(\n file,\n userId,\n directory,\n assignmentId,\n submissionId,\n announcementId,\n conversationId\n )\n );\n return await Promise.all(uploadPromises);\n } catch (error) {\n logger.error('Error creating direct upload files:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to create direct upload files',\n });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"c2b8462a-2a3e-5192-85cc-5f8cc649bfec"}
1
+ {"version":3,"file":"fileUpload.js","sources":["lib/fileUpload.ts"],"sourceRoot":"/","sourcesContent":["import { TRPCError } from \"@trpc/server\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { getSignedUrl, objectExists } from \"./googleCloudStorage.js\";\nimport { prisma } from \"./prisma.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { env } from \"./config/env.js\";\n\nexport interface FileData {\n name: string;\n type: string;\n size: number;\n // No data field - for direct file uploads\n}\n\nexport interface DirectFileData {\n name: string;\n type: string;\n size: number;\n // No data field - for direct file uploads\n}\n\nexport interface UploadedFile {\n id: string;\n name: string;\n type: string;\n size: number;\n path: string;\n thumbnailId?: string;\n}\n\nexport interface DirectUploadFile {\n id: string;\n name: string;\n type: string;\n size: number;\n path: string;\n uploadUrl: string;\n uploadExpiresAt: Date;\n uploadSessionId: string;\n}\n\nexport async function validateDirectUploadIntent(input: {\n filePath: string;\n uploadSessionId: string;\n userId: string;\n}) {\n const fileRecord = await prisma.file.findFirst({\n where: {\n path: input.filePath,\n uploadSessionId: input.uploadSessionId,\n userId: input.userId,\n uploadExpiresAt: { gt: new Date() },\n isOrphaned: false,\n messageId: null,\n },\n select: {\n id: true,\n uploadStatus: true,\n },\n });\n\n if (!fileRecord) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Invalid or expired upload intent',\n });\n }\n\n if (!['PENDING', 'UPLOADING'].includes(fileRecord.uploadStatus)) {\n throw new TRPCError({\n code: 'BAD_REQUEST',\n message: 'Upload is no longer accepting data',\n });\n }\n\n return fileRecord;\n}\n\n/**\n * Capability check for the `/api/upload/:filePath` proxy. The browser uploads\n * with no auth header (only the server-issued `uploadSessionId` in the URL),\n * so the session id + path pair *is* the capability: it maps to exactly one\n * pending, unexpired file record that the server created for the requesting\n * owner. Without this, the proxy would write the request body to any GCS\n * object path a caller names.\n */\nexport async function validateDirectUploadSession(input: {\n filePath: string;\n uploadSessionId: string | undefined;\n}) {\n if (!input.uploadSessionId) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Missing upload session',\n });\n }\n\n const fileRecord = await prisma.file.findFirst({\n where: {\n path: input.filePath,\n uploadSessionId: input.uploadSessionId,\n uploadExpiresAt: { gt: new Date() },\n isOrphaned: false,\n },\n select: {\n id: true,\n uploadStatus: true,\n },\n });\n\n if (!fileRecord) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'Invalid or expired upload intent',\n });\n }\n\n if (!['PENDING', 'UPLOADING'].includes(fileRecord.uploadStatus)) {\n throw new TRPCError({\n code: 'BAD_REQUEST',\n message: 'Upload is no longer accepting data',\n });\n }\n\n return fileRecord;\n}\n\n// DEPRECATED: These functions are no longer used - files are uploaded directly to GCS\n// Use createDirectUploadFile() and createDirectUploadFiles() instead\n\n/**\n * @deprecated Use createDirectUploadFile instead\n */\nexport async function uploadFile(\n file: FileData,\n userId: string,\n directory?: string,\n assignmentId?: string\n): Promise<UploadedFile> {\n throw new TRPCError({\n code: 'NOT_IMPLEMENTED',\n message: 'uploadFile is deprecated. Use createDirectUploadFile instead.',\n });\n}\n\n/**\n * @deprecated Use createDirectUploadFiles instead\n */\nexport async function uploadFiles(\n files: FileData[], \n userId: string,\n directory?: string\n): Promise<UploadedFile[]> {\n throw new TRPCError({\n code: 'NOT_IMPLEMENTED',\n message: 'uploadFiles is deprecated. Use createDirectUploadFiles instead.',\n });\n}\n\n/**\n * Gets a signed URL for a file\n * @param filePath The path of the file in Google Cloud Storage\n * @returns The signed URL\n */\nexport async function getFileUrl(filePath: string): Promise<string> {\n try {\n return await getSignedUrl(filePath);\n } catch (error) {\n console.error('Error getting signed URL:', error);\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to get file URL',\n });\n }\n}\n\n/**\n * Creates a file record for direct upload and generates signed URL\n * @param file The file metadata (no base64 data)\n * @param userId The ID of the user uploading the file\n * @param directory Optional directory to store the file in\n * @param assignmentId Optional assignment ID to associate the file with\n * @param submissionId Optional submission ID to associate the file with\n * @returns The direct upload file information with signed URL\n */\nexport async function createDirectUploadFile(\n file: DirectFileData,\n userId: string,\n directory?: string,\n assignmentId?: string,\n submissionId?: string,\n announcementId?: string,\n conversationId?: string\n): Promise<DirectUploadFile> {\n try {\n // Validate file extension matches MIME type\n const fileExtension = file.name.split('.').pop()?.toLowerCase();\n const mimeType = file.type.toLowerCase();\n \n const extensionMimeMap: Record<string, string[]> = {\n 'jpg': ['image/jpeg'],\n 'jpeg': ['image/jpeg'],\n 'png': ['image/png'],\n 'gif': ['image/gif'],\n 'webp': ['image/webp']\n };\n \n if (fileExtension && extensionMimeMap[fileExtension]) {\n if (!extensionMimeMap[fileExtension].includes(mimeType)) {\n throw new Error(`File extension .${fileExtension} does not match MIME type ${mimeType}`);\n }\n }\n \n // Create a unique filename\n const uniqueFilename = `${uuidv4()}.${fileExtension}`;\n \n // Construct the full path\n const filePath = directory \n ? `${directory}/${uniqueFilename}`\n : uniqueFilename;\n \n // Generate upload session ID\n const uploadSessionId = uuidv4();\n \n // Generate backend proxy upload URL (not direct GCS)\n const baseUrl = env.BACKEND_URL || 'http://localhost:3001';\n const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}?uploadSessionId=${encodeURIComponent(uploadSessionId)}`;\n const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now\n \n // Create file record in database with PENDING status\n const fileRecord = await prisma.file.create({\n data: {\n name: file.name,\n type: file.type,\n size: file.size,\n path: filePath,\n uploadStatus: 'PENDING',\n uploadUrl,\n uploadExpiresAt,\n uploadSessionId,\n uploadedAt: null,\n user: {\n connect: { id: userId }\n },\n ...(directory && {\n folder: {\n connect: {id: directory},\n },\n }),\n ...(assignmentId && {\n assignment: {\n connect: { id: assignmentId }\n }\n }),\n ...(submissionId && {\n submission: {\n connect: { id: submissionId }\n }\n }),\n ...(announcementId && {\n announcement: {\n connect: { id: announcementId }\n }\n }),\n ...(conversationId && {\n conversationId,\n })\n },\n });\n \n return {\n id: fileRecord.id,\n name: file.name,\n type: file.type,\n size: file.size,\n path: filePath,\n uploadUrl,\n uploadExpiresAt,\n uploadSessionId\n };\n } catch (error) {\n logger.error('Error creating direct upload file:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to create direct upload file',\n });\n }\n}\n\n/**\n * Confirms a direct upload was successful\n * @param fileId The ID of the file record\n * @param uploadSuccess Whether the upload was successful\n * @param errorMessage Optional error message if upload failed\n */\nexport async function confirmDirectUpload(\n fileId: string,\n uploadSuccess: boolean,\n errorMessage?: string\n): Promise<void> {\n try {\n // First fetch the file record to get the object path\n const fileRecord = await prisma.file.findUnique({\n where: { id: fileId },\n select: { path: true }\n });\n\n if (!fileRecord) {\n throw new TRPCError({\n code: 'NOT_FOUND',\n message: 'File record not found',\n });\n }\n\n let actualUploadSuccess = uploadSuccess;\n let actualErrorMessage = errorMessage;\n\n // If uploadSuccess is true, verify the object actually exists in GCS\n if (uploadSuccess) {\n try {\n const exists = await objectExists(env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);\n if (!exists) {\n actualUploadSuccess = false;\n actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';\n logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);\n }\n } catch (error) {\n logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n actualUploadSuccess = false;\n actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';\n }\n }\n\n const updateData: any = {\n uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',\n uploadProgress: actualUploadSuccess ? 100 : 0,\n uploadedAt: actualUploadSuccess ? new Date() : null,\n };\n \n if (!actualUploadSuccess && actualErrorMessage) {\n updateData.uploadError = actualErrorMessage;\n updateData.uploadRetryCount = { increment: 1 };\n }\n \n await prisma.file.update({\n where: { id: fileId },\n data: updateData\n });\n } catch (error) {\n logger.error('Error confirming direct upload:', {error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to confirm upload',\n });\n }\n}\n\n/**\n * Updates upload progress for a direct upload\n * @param fileId The ID of the file record\n * @param progress Progress percentage (0-100)\n */\nexport async function updateUploadProgress(\n fileId: string,\n progress: number\n): Promise<void> {\n try {\n // await prisma.file.update({\n // where: { id: fileId },\n // data: {\n // uploadStatus: 'UPLOADING',\n // uploadProgress: Math.min(100, Math.max(0, progress))\n // }\n // });\n const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });\n if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;\n const clamped = Math.min(100, Math.max(0, progress));\n await prisma.file.update({\n where: { id: fileId },\n data: {\n uploadStatus: 'UPLOADING',\n uploadProgress: clamped\n }\n });\n } catch (error) {\n logger.error('Error updating upload progress:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error}); \n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to update upload progress',\n });\n }\n}\n\n/**\n * Creates multiple direct upload files\n * @param files Array of file metadata\n * @param userId The ID of the user uploading the files\n * @param directory Optional subdirectory to store the files in\n * @param assignmentId Optional assignment ID to associate files with\n * @param submissionId Optional submission ID to associate files with\n * @returns Array of direct upload file information\n */\nexport async function createDirectUploadFiles(\n files: DirectFileData[], \n userId: string,\n directory?: string,\n assignmentId?: string,\n submissionId?: string,\n announcementId?: string,\n conversationId?: string\n): Promise<DirectUploadFile[]> {\n try {\n const uploadPromises = files.map(file => \n createDirectUploadFile(\n file,\n userId,\n directory,\n assignmentId,\n submissionId,\n announcementId,\n conversationId\n )\n );\n return await Promise.all(uploadPromises);\n } catch (error) {\n logger.error('Error creating direct upload files:', {error: error instanceof Error ? {\n name: error.name,\n message: error.message,\n stack: error.stack,\n } : error});\n throw new TRPCError({\n code: 'INTERNAL_SERVER_ERROR',\n message: 'Failed to create direct upload files',\n });\n }\n}\n"],"names":[],"mappings":"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","debug_id":"9afb6b2d-d7ed-55cd-b152-c73f70b17137"}
@@ -244,11 +244,11 @@ export declare const docToCreateSchema: z.ZodObject<{
244
244
  }, "strip", z.ZodTypeAny, {
245
245
  title: string;
246
246
  content: string;
247
- format: "jpg" | "jpeg" | "png" | "gif" | "html" | "json" | "pdf" | "docx" | "doc" | "xlsx" | "pptx" | "csv" | "md" | "txt" | "xml" | "tex" | "zip" | "tar" | "py" | "js" | "ts" | "css" | "sh";
247
+ format: "html" | "jpg" | "jpeg" | "png" | "gif" | "json" | "pdf" | "docx" | "doc" | "xlsx" | "pptx" | "csv" | "md" | "txt" | "xml" | "tex" | "zip" | "tar" | "py" | "js" | "ts" | "css" | "sh";
248
248
  }, {
249
249
  title: string;
250
250
  content: string;
251
- format?: "jpg" | "jpeg" | "png" | "gif" | "html" | "json" | "pdf" | "docx" | "doc" | "xlsx" | "pptx" | "csv" | "md" | "txt" | "xml" | "tex" | "zip" | "tar" | "py" | "js" | "ts" | "css" | "sh" | undefined;
251
+ format?: "html" | "jpg" | "jpeg" | "png" | "gif" | "json" | "pdf" | "docx" | "doc" | "xlsx" | "pptx" | "csv" | "md" | "txt" | "xml" | "tex" | "zip" | "tar" | "py" | "js" | "ts" | "css" | "sh" | undefined;
252
252
  }>;
253
253
  export declare const assignmentUpdateSchema: z.ZodObject<{
254
254
  id: z.ZodString;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Newton visualisation generator: turns a teacher's description into a
3
+ * self-contained interactive HTML document (sliders, canvas/SVG, labels)
4
+ * that the frontend renders inside a sandboxed iframe.
5
+ */
6
+ import { z } from "zod";
7
+ import { type AiUsageAttribution } from "../utils/inference.js";
8
+ export declare const VISUALIZATION_MODEL = "gpt-5-mini";
9
+ export declare const MAX_VISUALIZATION_HTML_LENGTH = 200000;
10
+ export declare const generatedVisualizationSchema: z.ZodObject<{
11
+ title: z.ZodString;
12
+ html: z.ZodString;
13
+ }, "strip", z.ZodTypeAny, {
14
+ title: string;
15
+ html: string;
16
+ }, {
17
+ title: string;
18
+ html: string;
19
+ }>;
20
+ export type GeneratedVisualization = z.infer<typeof generatedVisualizationSchema>;
21
+ export declare const VISUALIZATION_SYSTEM_PROMPT = "You are Newton, the teaching assistant inside the Studious LMS.\nA teacher wants an interactive visualisation to attach to a lesson for students.\n\nReturn ONE complete, self-contained HTML document:\n- All CSS in a <style> tag and all JavaScript in inline <script> tags. No external scripts, stylesheets, fonts, images or network requests (no fetch, XMLHttpRequest, WebSocket, import()). It runs in a sandboxed iframe with no network access.\n- No localStorage, sessionStorage, cookies, alert, prompt or confirm.\n- Use <canvas> or inline <svg> for the visual. Give students real controls (sliders, toggles, buttons, draggable points) that update the visual live, and show the key values numerically next to each control.\n- Label axes, units and important quantities. Include a one-sentence instruction at the top telling students what to explore.\n- Responsive: fill the viewport width, work from 320px to 1200px wide, no horizontal scroll. Use a clean neutral look with system-ui font and support prefers-color-scheme: dark.\n- The science/maths must be correct. Prefer accurate formulas over approximations, and clamp inputs to physically sensible ranges.\n- Keep it focused on what the teacher asked for; no quizzes, scoring or chat.";
22
+ interface GenerateOptions {
23
+ attribution?: AiUsageAttribution;
24
+ className?: string | null;
25
+ subject?: string | null;
26
+ }
27
+ export declare function generateVisualization(prompt: string, options?: GenerateOptions): Promise<GeneratedVisualization>;
28
+ export declare function reviseVisualization(current: {
29
+ title: string;
30
+ html: string;
31
+ }, instruction: string, options?: GenerateOptions): Promise<GeneratedVisualization>;
32
+ export {};
33
+ //# sourceMappingURL=generateVisualization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateVisualization.d.ts","sourceRoot":"/","sources":["pipelines/generateVisualization.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAa,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3E,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAChD,eAAO,MAAM,6BAA6B,SAAU,CAAC;AAErD,eAAO,MAAM,4BAA4B;;;;;;;;;EASvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,2BAA2B,4tCAUsC,CAAC;AAE/E,UAAU,eAAe;IACvB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAuBD,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC,CAiBjC;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACxC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,sBAAsB,CAAC,CAiBjC"}