@goscribe/server 1.5.0 → 1.6.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 (41) hide show
  1. package/dist/context.d.ts +14 -1
  2. package/dist/context.js +23 -2
  3. package/dist/lib/ai/index.d.ts +3 -2
  4. package/dist/lib/ai/index.js +3 -2
  5. package/dist/lib/ai/llm-client.d.ts +1 -0
  6. package/dist/lib/ai/llm-client.js +17 -0
  7. package/dist/routers/_app.d.ts +40 -80
  8. package/dist/routers/auth.js +1 -1
  9. package/dist/routers/flashcards.d.ts +12 -1
  10. package/dist/routers/payment.d.ts +1 -12
  11. package/dist/routers/workspace.d.ts +27 -67
  12. package/dist/routers/workspace.js +1 -0
  13. package/dist/services/billing/payment.service.d.ts +1 -12
  14. package/dist/services/billing/payment.service.js +3 -6
  15. package/dist/services/billing/usage.service.d.ts +30 -10
  16. package/dist/services/billing/usage.service.js +87 -15
  17. package/dist/services/content/copilot.service.js +15 -29
  18. package/dist/services/content/flashcard-progress.service.js +9 -9
  19. package/dist/services/content/flashcard.service.d.ts +45 -1
  20. package/dist/services/content/flashcard.service.js +81 -68
  21. package/dist/services/content/media-analysis.service.js +27 -27
  22. package/dist/services/content/worksheet-generation.service.test.js +2 -2
  23. package/dist/services/workspace/workspace.service.d.ts +23 -67
  24. package/dist/services/workspace/workspace.service.js +69 -62
  25. package/dist/trpc.d.ts +12 -4
  26. package/dist/trpc.js +5 -11
  27. package/package.json +1 -1
  28. package/src/context.ts +33 -3
  29. package/src/lib/ai/index.ts +3 -0
  30. package/src/lib/ai/llm-client.ts +23 -0
  31. package/src/routers/auth.ts +1 -1
  32. package/src/routers/workspace.ts +4 -0
  33. package/src/services/billing/payment.service.ts +3 -6
  34. package/src/services/billing/usage.service.ts +190 -77
  35. package/src/services/content/copilot.service.ts +23 -32
  36. package/src/services/content/flashcard-progress.service.ts +12 -9
  37. package/src/services/content/flashcard.service.ts +89 -66
  38. package/src/services/content/media-analysis.service.ts +34 -29
  39. package/src/services/content/worksheet-generation.service.test.ts +2 -2
  40. package/src/services/workspace/workspace.service.ts +73 -66
  41. package/src/trpc.ts +5 -13
@@ -7,6 +7,7 @@ import { ai } from '../../lib/ai/index.js';
7
7
  import { workspaceKbService } from '../workspace/workspace-kb.service.js';
8
8
  import { getUserUsage, getUserPlanLimits } from '../billing/usage.service.js';
9
9
  import { notifyArtifactFailed, notifyArtifactReady } from '../notifications/notification.service.js';
10
+ import { FlashcardService } from './flashcard.service.js';
10
11
  const PIPELINE_STEPS = ['fileUpload', 'fileAnalysis', 'studyGuide', 'flashcards'];
11
12
  const PROGRESS_FILENAME_MAX_CHARS = 20;
12
13
  function truncateProgressFilename(value, maxChars = PROGRESS_FILENAME_MAX_CHARS) {
@@ -312,16 +313,27 @@ export class MediaAnalysisService extends BaseService {
312
313
  }));
313
314
  const flashcardRag = await workspaceKbService.retrieveContext(input.workspaceId, 'flashcard key concepts facts definitions questions', 8);
314
315
  const content = await ai.backend.generateFlashcardQuestions(input.workspaceId, userId, 10, 'medium', undefined, { ragContext: flashcardRag });
315
- const artifact = await this.db.artifact.create({
316
- data: {
317
- workspaceId: input.workspaceId,
318
- type: ArtifactType.FLASHCARD_SET,
319
- title: files.length === 1
320
- ? `Flashcards - ${primaryFile.name}`
321
- : `Flashcards - ${files.length} files`,
322
- createdById: userId,
323
- },
316
+ const flashcardService = new FlashcardService(this.db);
317
+ const primarySet = await flashcardService.ensurePrimarySet(userId, input.workspaceId);
318
+ const orderOffset = primarySet.flashcards.reduce((max, card) => Math.max(max, card.order), -1);
319
+ const flashcardTitle = files.length === 1
320
+ ? `Flashcards - ${primaryFile.name}`
321
+ : `Flashcards - ${files.length} files`;
322
+ await this.db.artifact.update({
323
+ where: { id: primarySet.id },
324
+ data: { title: flashcardTitle },
324
325
  });
326
+ const appendCard = async (front, back, index) => {
327
+ await this.db.flashcard.create({
328
+ data: {
329
+ artifactId: primarySet.id,
330
+ front,
331
+ back,
332
+ order: orderOffset + 1 + index,
333
+ tags: ['ai-generated', 'medium'],
334
+ },
335
+ });
336
+ };
325
337
  try {
326
338
  const parsed = typeof content === 'string' ? JSON.parse(content) : content;
327
339
  const flashcardData = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);
@@ -329,15 +341,7 @@ export class MediaAnalysisService extends BaseService {
329
341
  const card = flashcardData[i];
330
342
  const front = card.term || card.front || card.question || card.prompt || `Question ${i + 1}`;
331
343
  const back = card.definition || card.back || card.answer || card.solution || `Answer ${i + 1}`;
332
- await this.db.flashcard.create({
333
- data: {
334
- artifactId: artifact.id,
335
- front: front,
336
- back: back,
337
- order: i,
338
- tags: ['ai-generated', 'medium'],
339
- },
340
- });
344
+ await appendCard(front, back, i);
341
345
  }
342
346
  }
343
347
  catch (parseError) {
@@ -347,18 +351,14 @@ export class MediaAnalysisService extends BaseService {
347
351
  const line = lines[i];
348
352
  if (line.includes(' - ')) {
349
353
  const [front, back] = line.split(' - ');
350
- await this.db.flashcard.create({
351
- data: {
352
- artifactId: artifact.id,
353
- front: front.trim(),
354
- back: back.trim(),
355
- order: i,
356
- tags: ['ai-generated', 'medium'],
357
- },
358
- });
354
+ await appendCard(front.trim(), back.trim(), i);
359
355
  }
360
356
  }
361
357
  }
358
+ const artifact = await this.db.artifact.findUniqueOrThrow({
359
+ where: { id: primarySet.id },
360
+ include: { flashcards: true },
361
+ });
362
362
  results.artifacts.flashcards = artifact;
363
363
  await PusherService.emitFlashcardComplete(input.workspaceId, artifact);
364
364
  await notifyArtifactReady(this.db, {
@@ -4,8 +4,8 @@ import { mergeWorksheetGenerationConfig, normalizeWorksheetProblemForDb } from '
4
4
  test('mergeWorksheetGenerationConfig: override beats preset and legacy', () => {
5
5
  const r = mergeWorksheetGenerationConfig({ mode: 'practice', numQuestions: 5, difficulty: 'easy' }, { mode: 'quiz', numQuestions: 10, difficulty: 'medium' }, { numQuestions: 3, difficulty: 'hard' });
6
6
  assert.equal(r.mode, 'quiz');
7
- assert.equal(r.numQuestions, 10);
8
- assert.equal(r.difficulty, 'medium');
7
+ assert.equal(r.numQuestions, 3);
8
+ assert.equal(r.difficulty, 'hard');
9
9
  });
10
10
  test('normalizeWorksheetProblemForDb coerces MCQ answer to option index', () => {
11
11
  const row = normalizeWorksheetProblemForDb({
@@ -33,35 +33,26 @@ export declare class WorkspaceService extends BaseService {
33
33
  folders: {
34
34
  name: string;
35
35
  id: string;
36
- createdAt: Date;
37
36
  updatedAt: Date;
38
- ownerId: string;
39
37
  parentId: string | null;
40
38
  color: string;
41
39
  markerColor: string | null;
42
40
  }[];
43
- workspaces: ({
44
- uploads: {
45
- name: string;
46
- id: string;
47
- createdAt: Date;
48
- mimeType: string;
49
- }[];
50
- } & {
41
+ workspaces: {
51
42
  id: string;
52
- createdAt: Date;
53
43
  updatedAt: Date;
54
44
  title: string;
55
- description: string | null;
56
- ownerId: string;
57
45
  color: string;
58
46
  markerColor: string | null;
59
47
  icon: string;
60
48
  folderId: string | null;
61
- fileBeingAnalyzed: boolean;
62
- analysisProgress: import("@prisma/client/runtime/library").JsonValue | null;
63
- needsAnalysis: boolean;
64
- })[];
49
+ uploads: {
50
+ name: string;
51
+ id: string;
52
+ createdAt: Date;
53
+ mimeType: string;
54
+ }[];
55
+ }[];
65
56
  }>;
66
57
  create(userId: string, input: {
67
58
  name: string;
@@ -122,70 +113,35 @@ export declare class WorkspaceService extends BaseService {
122
113
  markerColor: string | null;
123
114
  }>;
124
115
  get(userId: string, id: string): Promise<{
116
+ id: string;
117
+ createdAt: Date;
118
+ updatedAt: Date;
119
+ title: string;
120
+ description: string | null;
125
121
  folder: {
126
122
  name: string;
127
123
  id: string;
128
- createdAt: Date;
129
- updatedAt: Date;
130
- ownerId: string;
131
- parentId: string | null;
132
124
  color: string;
133
- markerColor: string | null;
134
125
  } | null;
135
- artifacts: {
136
- id: string;
137
- createdAt: Date;
138
- updatedAt: Date;
139
- workspaceId: string;
140
- type: import("@prisma/client").$Enums.ArtifactType;
141
- title: string;
142
- isArchived: boolean;
143
- difficulty: import("@prisma/client").$Enums.Difficulty | null;
144
- estimatedTime: string | null;
145
- createdById: string | null;
146
- description: string | null;
147
- generating: boolean;
148
- generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
149
- worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
150
- imageObjectKey: string | null;
151
- }[];
126
+ ownerId: string;
127
+ color: string;
128
+ markerColor: string | null;
129
+ icon: string;
130
+ folderId: string | null;
131
+ fileBeingAnalyzed: boolean;
132
+ analysisProgress: import("@prisma/client/runtime/library").JsonValue;
133
+ needsAnalysis: boolean;
152
134
  uploads: {
153
- meta: import("@prisma/client/runtime/library").JsonValue | null;
154
135
  name: string;
155
136
  id: string;
156
137
  createdAt: Date;
157
- workspaceId: string | null;
158
- userId: string | null;
159
138
  mimeType: string;
160
139
  size: number;
161
- bucket: string | null;
162
140
  objectKey: string | null;
163
- url: string | null;
164
- checksum: string | null;
165
- aiTranscription: import("@prisma/client/runtime/library").JsonValue | null;
166
141
  }[];
167
- } & {
168
- id: string;
169
- createdAt: Date;
170
- updatedAt: Date;
171
- title: string;
172
- description: string | null;
173
- ownerId: string;
174
- color: string;
175
- markerColor: string | null;
176
- icon: string;
177
- folderId: string | null;
178
- fileBeingAnalyzed: boolean;
179
- analysisProgress: import("@prisma/client/runtime/library").JsonValue | null;
180
- needsAnalysis: boolean;
181
- }>;
182
- getStats(userId: string): Promise<{
183
- workspaces: number;
184
- folders: number;
185
- lastUpdated: Date | undefined;
186
- spaceUsed: number;
187
- spaceTotal: number;
188
142
  }>;
143
+ getAccountSummary(userId: string): Promise<import("../billing/usage.service.js").AccountSummary>;
144
+ getStats(userId: string): Promise<import("../billing/usage.service.js").AccountStats>;
189
145
  update(userId: string, input: {
190
146
  id: string;
191
147
  name?: string;
@@ -1,10 +1,10 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { BaseService } from '../base.service.js';
3
- import { ArtifactType } from '../../lib/constants.js';
4
3
  import { supabaseClient } from '../../lib/storage.js';
5
4
  import PusherService from '../../lib/pusher.js';
6
5
  import { ai } from '../../lib/ai/index.js';
7
6
  import { workspaceKbService } from './workspace-kb.service.js';
7
+ import { getAccountSummary, invalidateUserBillingCache, } from '../billing/usage.service.js';
8
8
  import { getUserStorageLimit } from '../billing/subscription.service.js';
9
9
  import { notifyWorkspaceDeleted } from '../notifications/notification.service.js';
10
10
  export class WorkspaceService extends BaseService {
@@ -22,24 +22,41 @@ export class WorkspaceService extends BaseService {
22
22
  return { workspaces, folders };
23
23
  }
24
24
  async getTree(userId) {
25
- const allFolders = await this.db.folder.findMany({
26
- where: { ownerId: userId },
27
- orderBy: { updatedAt: 'desc' },
28
- });
29
- const allWorkspaces = await this.db.workspace.findMany({
30
- where: { ownerId: userId },
31
- include: {
32
- uploads: {
33
- select: {
34
- id: true,
35
- name: true,
36
- mimeType: true,
37
- createdAt: true,
25
+ const [allFolders, allWorkspaces] = await Promise.all([
26
+ this.db.folder.findMany({
27
+ where: { ownerId: userId },
28
+ select: {
29
+ id: true,
30
+ name: true,
31
+ parentId: true,
32
+ color: true,
33
+ markerColor: true,
34
+ updatedAt: true,
35
+ },
36
+ orderBy: { updatedAt: 'desc' },
37
+ }),
38
+ this.db.workspace.findMany({
39
+ where: { ownerId: userId },
40
+ select: {
41
+ id: true,
42
+ title: true,
43
+ folderId: true,
44
+ icon: true,
45
+ color: true,
46
+ markerColor: true,
47
+ updatedAt: true,
48
+ uploads: {
49
+ select: {
50
+ id: true,
51
+ name: true,
52
+ mimeType: true,
53
+ createdAt: true,
54
+ },
38
55
  },
39
56
  },
40
- },
41
- orderBy: { updatedAt: 'desc' },
42
- });
57
+ orderBy: { updatedAt: 'desc' },
58
+ }),
59
+ ]);
43
60
  return { folders: allFolders, workspaces: allWorkspaces };
44
61
  }
45
62
  async create(userId, input) {
@@ -50,27 +67,16 @@ export class WorkspaceService extends BaseService {
50
67
  ownerId: userId,
51
68
  folderId: input.parentId ?? null,
52
69
  ...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
53
- artifacts: {
54
- create: {
55
- type: ArtifactType.FLASHCARD_SET,
56
- title: 'New Flashcard Set',
57
- },
58
- createMany: {
59
- data: [
60
- { type: ArtifactType.WORKSHEET, title: 'Worksheet 1' },
61
- { type: ArtifactType.WORKSHEET, title: 'Worksheet 2' },
62
- ],
63
- },
64
- },
65
70
  },
66
71
  });
67
- await ai.backend.initSession(ws.id, userId).catch((err) => {
72
+ void ai.backend.initSession(ws.id, userId).catch((err) => {
68
73
  this.logger.error('Failed to init AI session on workspace creation:', err);
69
74
  });
70
- await workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
75
+ void workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
71
76
  this.logger.error('Failed to create workspace knowledge base:', err);
72
77
  });
73
- await PusherService.emitLibraryUpdate(userId);
78
+ invalidateUserBillingCache(userId);
79
+ void PusherService.emitLibraryUpdate(userId);
74
80
  return ws;
75
81
  }
76
82
  async createFolder(userId, input) {
@@ -101,43 +107,44 @@ export class WorkspaceService extends BaseService {
101
107
  async get(userId, id) {
102
108
  const ws = await this.db.workspace.findFirst({
103
109
  where: { id, ownerId: userId },
104
- include: {
105
- artifacts: true,
106
- folder: true,
107
- uploads: true,
110
+ select: {
111
+ id: true,
112
+ title: true,
113
+ description: true,
114
+ icon: true,
115
+ color: true,
116
+ markerColor: true,
117
+ ownerId: true,
118
+ folderId: true,
119
+ fileBeingAnalyzed: true,
120
+ analysisProgress: true,
121
+ needsAnalysis: true,
122
+ createdAt: true,
123
+ updatedAt: true,
124
+ folder: { select: { id: true, name: true, color: true } },
125
+ uploads: {
126
+ select: {
127
+ id: true,
128
+ name: true,
129
+ mimeType: true,
130
+ size: true,
131
+ createdAt: true,
132
+ objectKey: true,
133
+ },
134
+ orderBy: { createdAt: 'desc' },
135
+ },
108
136
  },
109
137
  });
110
138
  if (!ws)
111
139
  throw new TRPCError({ code: 'NOT_FOUND' });
112
140
  return ws;
113
141
  }
142
+ async getAccountSummary(userId) {
143
+ return getAccountSummary(userId);
144
+ }
114
145
  async getStats(userId) {
115
- const workspaces = await this.db.workspace.findMany({
116
- where: {
117
- OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
118
- },
119
- });
120
- const folders = await this.db.folder.findMany({
121
- where: { OR: [{ ownerId: userId }] },
122
- });
123
- const lastUpdated = await this.db.workspace.findFirst({
124
- where: {
125
- OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
126
- },
127
- orderBy: { updatedAt: 'desc' },
128
- });
129
- const spaceLeft = await this.db.fileAsset.aggregate({
130
- where: { workspaceId: { in: workspaces.map((ws) => ws.id) }, userId },
131
- _sum: { size: true },
132
- });
133
- const storageLimit = await getUserStorageLimit(userId);
134
- return {
135
- workspaces: workspaces.length,
136
- folders: folders.length,
137
- lastUpdated: lastUpdated?.updatedAt,
138
- spaceUsed: spaceLeft._sum?.size ?? 0,
139
- spaceTotal: storageLimit,
140
- };
146
+ const summary = await getAccountSummary(userId);
147
+ return summary.stats;
141
148
  }
142
149
  async update(userId, input) {
143
150
  const existed = await this.db.workspace.findFirst({
package/dist/trpc.d.ts CHANGED
@@ -20,7 +20,9 @@ export declare const publicProcedure: import("@trpc/server").TRPCProcedureBuilde
20
20
  /** Exported procedures with middleware */
21
21
  export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
22
22
  userId: any;
23
- session: any;
23
+ session: {
24
+ user: import("./context.js").SessionUser;
25
+ };
24
26
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
25
27
  res: import("express").Response<any, Record<string, any>>;
26
28
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
@@ -28,7 +30,9 @@ export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilde
28
30
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
29
31
  export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
30
32
  userId: any;
31
- session: any;
33
+ session: {
34
+ user: import("./context.js").SessionUser;
35
+ };
32
36
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
33
37
  res: import("express").Response<any, Record<string, any>>;
34
38
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
@@ -36,7 +40,9 @@ export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuil
36
40
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
37
41
  export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
38
42
  userId: any;
39
- session: any;
43
+ session: {
44
+ user: import("./context.js").SessionUser;
45
+ };
40
46
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
41
47
  res: import("express").Response<any, Record<string, any>>;
42
48
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
@@ -44,7 +50,9 @@ export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder
44
50
  }, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
45
51
  export declare const limitedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
46
52
  userId: any;
47
- session: any;
53
+ session: {
54
+ user: import("./context.js").SessionUser;
55
+ };
48
56
  req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
49
57
  res: import("express").Response<any, Record<string, any>>;
50
58
  db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
package/dist/trpc.js CHANGED
@@ -79,11 +79,8 @@ const errorHandler = middleware(async ({ next }) => {
79
79
  /**
80
80
  * Middleware that enforces email verification
81
81
  */
82
- const isVerified = middleware(async ({ ctx, next }) => {
83
- const user = await ctx.db.user.findUnique({
84
- where: { id: ctx.session.user.id },
85
- select: { emailVerified: true },
86
- });
82
+ const isVerified = middleware(({ ctx, next }) => {
83
+ const user = ctx.session?.user;
87
84
  if (!user?.emailVerified) {
88
85
  throw new TRPCError({
89
86
  code: "FORBIDDEN",
@@ -139,12 +136,9 @@ const checkUsageLimit = middleware(async ({ ctx, next, path }) => {
139
136
  /**
140
137
  * Middleware that enforces system admin role
141
138
  */
142
- const isAdmin = middleware(async ({ ctx, next }) => {
143
- const user = await ctx.db.user.findUnique({
144
- where: { id: ctx.session.user.id },
145
- include: { role: true },
146
- });
147
- if (user?.role?.name !== 'System Admin') {
139
+ const isAdmin = middleware(({ ctx, next }) => {
140
+ const user = ctx.session?.user;
141
+ if (!user?.isSystemAdmin) {
148
142
  throw new TRPCError({
149
143
  code: "FORBIDDEN",
150
144
  message: "You do not have permission to access this resource",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goscribe/server",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/context.ts CHANGED
@@ -4,14 +4,44 @@ import { prisma } from "./lib/prisma.js";
4
4
  import { verifyCustomAuthCookie } from "./lib/auth.js";
5
5
  import cookie from "cookie";
6
6
 
7
+ export type SessionUser = {
8
+ id: string;
9
+ emailVerified: boolean;
10
+ isSystemAdmin: boolean;
11
+ };
12
+
7
13
  export async function createContext({ req, res }: CreateExpressContextOptions) {
8
14
  const cookies = cookie.parse(req.headers.cookie ?? "");
9
15
 
10
- // Only use custom auth cookie
11
16
  const custom = verifyCustomAuthCookie(cookies["auth_token"]);
12
-
17
+
13
18
  if (custom) {
14
- return { db: prisma, session: { user: { id: custom.userId } } as any, req, res, cookies };
19
+ const user = await prisma.user.findUnique({
20
+ where: { id: custom.userId },
21
+ select: {
22
+ id: true,
23
+ emailVerified: true,
24
+ role: { select: { name: true } },
25
+ },
26
+ });
27
+
28
+ if (!user) {
29
+ return { db: prisma, session: null, req, res, cookies };
30
+ }
31
+
32
+ const sessionUser: SessionUser = {
33
+ id: user.id,
34
+ emailVerified: !!user.emailVerified,
35
+ isSystemAdmin: user.role?.name === "System Admin",
36
+ };
37
+
38
+ return {
39
+ db: prisma,
40
+ session: { user: sessionUser } as { user: SessionUser },
41
+ req,
42
+ res,
43
+ cookies,
44
+ };
15
45
  }
16
46
 
17
47
  return { db: prisma, session: null, req, res, cookies };
@@ -2,6 +2,7 @@ import { aiConfig } from './config.js';
2
2
  import {
3
3
  complete,
4
4
  completeText,
5
+ streamComplete,
5
6
  } from './llm-client.js';
6
7
  import {
7
8
  DEFAULT_EMBEDDING_DIM,
@@ -21,6 +22,7 @@ export const ai = {
21
22
  llm: {
22
23
  complete,
23
24
  completeText,
25
+ streamComplete,
24
26
  },
25
27
  embed: {
26
28
  texts: embedTexts,
@@ -49,6 +51,7 @@ export {
49
51
  aiConfig,
50
52
  complete,
51
53
  completeText,
54
+ streamComplete,
52
55
  DEFAULT_EMBEDDING_DIM,
53
56
  DEFAULT_EMBEDDING_MODEL,
54
57
  embedQuery,
@@ -27,5 +27,28 @@ export async function completeText(
27
27
  return response.choices?.[0]?.message?.content ?? '';
28
28
  }
29
29
 
30
+ export async function streamComplete(
31
+ messages: ChatMessage[],
32
+ onDelta: (delta: string) => void | Promise<void>,
33
+ options: CompleteOptions = {},
34
+ ): Promise<string> {
35
+ const stream = await client.chat.completions.create({
36
+ model: options.model ?? aiConfig.llm.model,
37
+ messages,
38
+ stream: true,
39
+ ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
40
+ });
41
+
42
+ let content = '';
43
+ for await (const chunk of stream) {
44
+ const delta = chunk.choices?.[0]?.delta?.content ?? '';
45
+ if (!delta) continue;
46
+ content += delta;
47
+ await onDelta(delta);
48
+ }
49
+
50
+ return content;
51
+ }
52
+
30
53
  /** @deprecated Use `complete()` from the ai module instead. */
31
54
  export default complete;
@@ -30,7 +30,7 @@ const passwordFieldSchema = z
30
30
  .regex(/[^a-zA-Z0-9]/, 'Password must contain at least one special character');
31
31
 
32
32
  export const auth = router({
33
- updateProfile: publicProcedure
33
+ updateProfile: authedProcedure
34
34
  .input(z.object({ name: z.string().min(1) }))
35
35
  .mutation(({ ctx, input }) =>
36
36
  new AuthService(ctx.db).updateProfile(ctx.session.user.id, input),
@@ -71,6 +71,10 @@ export const workspace = router({
71
71
  new WorkspaceService(ctx.db).getStats(ctx.session.user.id),
72
72
  ),
73
73
 
74
+ getAccountSummary: authedProcedure.query(({ ctx }) =>
75
+ new WorkspaceService(ctx.db).getAccountSummary(ctx.session.user.id),
76
+ ),
77
+
74
78
  getStudyAnalytics: authedProcedure.query(({ ctx }) =>
75
79
  new WorkspaceAnalyticsService(ctx.db).getStudyAnalytics(ctx.session.user.id),
76
80
  ),
@@ -5,7 +5,7 @@ import { Stripe } from 'stripe';
5
5
  import { BaseService } from '../base.service.js';
6
6
  import { stripe } from '../../lib/stripe.js';
7
7
  import { env } from '../../lib/env.js';
8
- import { getUserUsage, getUserPlanLimits } from './usage.service.js';
8
+ import { getAccountSummary, getUserUsage, getUserPlanLimits } from './usage.service.js';
9
9
  import {
10
10
  notifyPaymentSucceeded,
11
11
  notifySubscriptionActivated,
@@ -423,11 +423,8 @@ export class PaymentService extends BaseService {
423
423
  }
424
424
 
425
425
  async getUsageOverview(userId: string) {
426
- const [usage, limits] = await Promise.all([
427
- getUserUsage(userId),
428
- getUserPlanLimits(userId),
429
- ]);
430
- return { usage, limits, hasActivePlan: !!limits };
426
+ const { usage, limits, hasActivePlan } = await getAccountSummary(userId);
427
+ return { usage, limits, hasActivePlan };
431
428
  }
432
429
 
433
430
  getResourcePrices() {