@goscribe/server 1.0.10 → 1.1.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 (83) hide show
  1. package/ANALYSIS_PROGRESS_SPEC.md +463 -0
  2. package/PROGRESS_QUICK_REFERENCE.md +239 -0
  3. package/dist/lib/ai-session.d.ts +20 -9
  4. package/dist/lib/ai-session.js +316 -80
  5. package/dist/lib/auth.d.ts +35 -2
  6. package/dist/lib/auth.js +88 -15
  7. package/dist/lib/env.d.ts +32 -0
  8. package/dist/lib/env.js +46 -0
  9. package/dist/lib/errors.d.ts +33 -0
  10. package/dist/lib/errors.js +78 -0
  11. package/dist/lib/inference.d.ts +4 -1
  12. package/dist/lib/inference.js +9 -11
  13. package/dist/lib/logger.d.ts +62 -0
  14. package/dist/lib/logger.js +342 -0
  15. package/dist/lib/podcast-prompts.d.ts +43 -0
  16. package/dist/lib/podcast-prompts.js +135 -0
  17. package/dist/lib/pusher.d.ts +1 -0
  18. package/dist/lib/pusher.js +14 -2
  19. package/dist/lib/storage.d.ts +3 -3
  20. package/dist/lib/storage.js +51 -47
  21. package/dist/lib/validation.d.ts +51 -0
  22. package/dist/lib/validation.js +64 -0
  23. package/dist/routers/_app.d.ts +697 -111
  24. package/dist/routers/_app.js +5 -0
  25. package/dist/routers/auth.d.ts +11 -1
  26. package/dist/routers/chat.d.ts +11 -1
  27. package/dist/routers/flashcards.d.ts +205 -6
  28. package/dist/routers/flashcards.js +144 -66
  29. package/dist/routers/members.d.ts +165 -0
  30. package/dist/routers/members.js +531 -0
  31. package/dist/routers/podcast.d.ts +78 -63
  32. package/dist/routers/podcast.js +330 -393
  33. package/dist/routers/studyguide.d.ts +11 -1
  34. package/dist/routers/worksheets.d.ts +124 -13
  35. package/dist/routers/worksheets.js +123 -50
  36. package/dist/routers/workspace.d.ts +213 -26
  37. package/dist/routers/workspace.js +303 -181
  38. package/dist/server.js +12 -4
  39. package/dist/services/flashcard-progress.service.d.ts +183 -0
  40. package/dist/services/flashcard-progress.service.js +383 -0
  41. package/dist/services/flashcard.service.d.ts +183 -0
  42. package/dist/services/flashcard.service.js +224 -0
  43. package/dist/services/podcast-segment-reorder.d.ts +0 -0
  44. package/dist/services/podcast-segment-reorder.js +107 -0
  45. package/dist/services/podcast.service.d.ts +0 -0
  46. package/dist/services/podcast.service.js +326 -0
  47. package/dist/services/worksheet.service.d.ts +0 -0
  48. package/dist/services/worksheet.service.js +295 -0
  49. package/dist/trpc.d.ts +13 -2
  50. package/dist/trpc.js +55 -6
  51. package/dist/types/index.d.ts +126 -0
  52. package/dist/types/index.js +1 -0
  53. package/package.json +3 -2
  54. package/prisma/schema.prisma +142 -4
  55. package/src/lib/ai-session.ts +356 -85
  56. package/src/lib/auth.ts +113 -19
  57. package/src/lib/env.ts +59 -0
  58. package/src/lib/errors.ts +92 -0
  59. package/src/lib/inference.ts +11 -11
  60. package/src/lib/logger.ts +405 -0
  61. package/src/lib/pusher.ts +15 -3
  62. package/src/lib/storage.ts +56 -51
  63. package/src/lib/validation.ts +75 -0
  64. package/src/routers/_app.ts +5 -0
  65. package/src/routers/chat.ts +2 -23
  66. package/src/routers/flashcards.ts +108 -24
  67. package/src/routers/members.ts +586 -0
  68. package/src/routers/podcast.ts +385 -420
  69. package/src/routers/worksheets.ts +117 -35
  70. package/src/routers/workspace.ts +328 -195
  71. package/src/server.ts +13 -4
  72. package/src/services/flashcard-progress.service.ts +541 -0
  73. package/src/trpc.ts +59 -6
  74. package/src/types/index.ts +165 -0
  75. package/AUTH_FRONTEND_SPEC.md +0 -21
  76. package/CHAT_FRONTEND_SPEC.md +0 -474
  77. package/DATABASE_SETUP.md +0 -165
  78. package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
  79. package/PODCAST_FRONTEND_SPEC.md +0 -595
  80. package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
  81. package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
  82. package/WORKSPACE_FRONTEND_SPEC.md +0 -47
  83. package/test-ai-integration.js +0 -134
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Members router for workspace member management
3
+ *
4
+ * Features:
5
+ * - Get workspace members
6
+ * - Invite new members via email
7
+ * - Accept invitations via UUID
8
+ * - Change member roles
9
+ * - Remove members
10
+ * - Get current user's role
11
+ */
12
+ export declare const members: import("@trpc/server").TRPCBuiltRouter<{
13
+ ctx: {
14
+ db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
15
+ session: any;
16
+ req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
17
+ res: import("express").Response<any, Record<string, any>>;
18
+ cookies: Record<string, string | undefined>;
19
+ };
20
+ meta: object;
21
+ errorShape: {
22
+ data: {
23
+ zodError: string | null;
24
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
25
+ httpStatus: number;
26
+ path?: string;
27
+ stack?: string;
28
+ };
29
+ message: string;
30
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
31
+ };
32
+ transformer: true;
33
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
34
+ /**
35
+ * Get all members of a workspace
36
+ */
37
+ getMembers: import("@trpc/server").TRPCQueryProcedure<{
38
+ input: {
39
+ workspaceId: string;
40
+ };
41
+ output: ({
42
+ id: string;
43
+ name: string;
44
+ email: string;
45
+ image: string | null;
46
+ role: "admin" | "member";
47
+ joinedAt: Date;
48
+ } | {
49
+ id: string;
50
+ name: string;
51
+ email: string;
52
+ image: string | null;
53
+ role: "owner";
54
+ joinedAt: Date;
55
+ })[];
56
+ meta: object;
57
+ }>;
58
+ /**
59
+ * Get current user's role in a workspace
60
+ */
61
+ getCurrentUserRole: import("@trpc/server").TRPCQueryProcedure<{
62
+ input: {
63
+ workspaceId: string;
64
+ };
65
+ output: "admin" | "member" | "owner";
66
+ meta: object;
67
+ }>;
68
+ /**
69
+ * Invite a new member to the workspace
70
+ */
71
+ inviteMember: import("@trpc/server").TRPCMutationProcedure<{
72
+ input: {
73
+ workspaceId: string;
74
+ email: string;
75
+ role?: "admin" | "member" | undefined;
76
+ };
77
+ output: {
78
+ invitationId: string;
79
+ token: string;
80
+ email: string;
81
+ role: string;
82
+ expiresAt: Date;
83
+ workspaceTitle: string;
84
+ invitedByName: string | null;
85
+ };
86
+ meta: object;
87
+ }>;
88
+ /**
89
+ * Accept an invitation (public endpoint)
90
+ */
91
+ acceptInvite: import("@trpc/server").TRPCMutationProcedure<{
92
+ input: {
93
+ token: string;
94
+ };
95
+ output: {
96
+ workspaceId: string;
97
+ workspaceTitle: string;
98
+ role: string;
99
+ ownerName: string | null;
100
+ };
101
+ meta: object;
102
+ }>;
103
+ /**
104
+ * Change a member's role (owner only)
105
+ */
106
+ changeMemberRole: import("@trpc/server").TRPCMutationProcedure<{
107
+ input: {
108
+ workspaceId: string;
109
+ memberId: string;
110
+ role: "admin" | "member";
111
+ };
112
+ output: {
113
+ memberId: string;
114
+ role: "admin" | "member";
115
+ memberName: string | null;
116
+ message: string;
117
+ };
118
+ meta: object;
119
+ }>;
120
+ /**
121
+ * Remove a member from the workspace (owner only)
122
+ */
123
+ removeMember: import("@trpc/server").TRPCMutationProcedure<{
124
+ input: {
125
+ workspaceId: string;
126
+ memberId: string;
127
+ };
128
+ output: {
129
+ memberId: string;
130
+ message: string;
131
+ };
132
+ meta: object;
133
+ }>;
134
+ /**
135
+ * Get pending invitations for a workspace (owner only)
136
+ */
137
+ getPendingInvitations: import("@trpc/server").TRPCQueryProcedure<{
138
+ input: {
139
+ workspaceId: string;
140
+ };
141
+ output: {
142
+ id: string;
143
+ email: string;
144
+ role: string;
145
+ token: string;
146
+ expiresAt: Date;
147
+ createdAt: Date;
148
+ invitedByName: string | null;
149
+ }[];
150
+ meta: object;
151
+ }>;
152
+ /**
153
+ * Cancel a pending invitation (owner only)
154
+ */
155
+ cancelInvitation: import("@trpc/server").TRPCMutationProcedure<{
156
+ input: {
157
+ invitationId: string;
158
+ };
159
+ output: {
160
+ invitationId: string;
161
+ message: string;
162
+ };
163
+ meta: object;
164
+ }>;
165
+ }>>;
@@ -0,0 +1,531 @@
1
+ import { z } from 'zod';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { router, publicProcedure, authedProcedure } from '../trpc.js';
4
+ import { logger } from '../lib/logger.js';
5
+ /**
6
+ * Members router for workspace member management
7
+ *
8
+ * Features:
9
+ * - Get workspace members
10
+ * - Invite new members via email
11
+ * - Accept invitations via UUID
12
+ * - Change member roles
13
+ * - Remove members
14
+ * - Get current user's role
15
+ */
16
+ export const members = router({
17
+ /**
18
+ * Get all members of a workspace
19
+ */
20
+ getMembers: authedProcedure
21
+ .input(z.object({
22
+ workspaceId: z.string(),
23
+ }))
24
+ .query(async ({ ctx, input }) => {
25
+ // Check if user has access to this workspace
26
+ const workspace = await ctx.db.workspace.findFirst({
27
+ where: {
28
+ id: input.workspaceId,
29
+ OR: [
30
+ { ownerId: ctx.session.user.id },
31
+ { members: { some: { userId: ctx.session.user.id } } }
32
+ ]
33
+ },
34
+ include: {
35
+ owner: {
36
+ select: {
37
+ id: true,
38
+ name: true,
39
+ email: true,
40
+ image: true,
41
+ }
42
+ },
43
+ members: {
44
+ include: {
45
+ user: {
46
+ select: {
47
+ id: true,
48
+ name: true,
49
+ email: true,
50
+ image: true,
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ });
57
+ if (!workspace) {
58
+ throw new TRPCError({
59
+ code: 'NOT_FOUND',
60
+ message: 'Workspace not found or access denied'
61
+ });
62
+ }
63
+ // Format members with roles
64
+ const members = [
65
+ {
66
+ id: workspace.owner.id,
67
+ name: workspace.owner.name || 'Unknown',
68
+ email: workspace.owner.email || '',
69
+ image: workspace.owner.image,
70
+ role: 'owner',
71
+ joinedAt: workspace.createdAt,
72
+ },
73
+ ...workspace.members.map(membership => ({
74
+ id: membership.user.id,
75
+ name: membership.user.name || 'Unknown',
76
+ email: membership.user.email || '',
77
+ image: membership.user.image,
78
+ role: membership.role,
79
+ joinedAt: membership.joinedAt,
80
+ }))
81
+ ];
82
+ return members;
83
+ }),
84
+ /**
85
+ * Get current user's role in a workspace
86
+ */
87
+ getCurrentUserRole: authedProcedure
88
+ .input(z.object({
89
+ workspaceId: z.string(),
90
+ }))
91
+ .query(async ({ ctx, input }) => {
92
+ const workspace = await ctx.db.workspace.findFirst({
93
+ where: { id: input.workspaceId },
94
+ select: {
95
+ ownerId: true,
96
+ members: {
97
+ where: { userId: ctx.session.user.id },
98
+ select: { role: true }
99
+ }
100
+ }
101
+ });
102
+ if (!workspace) {
103
+ throw new TRPCError({
104
+ code: 'NOT_FOUND',
105
+ message: 'Workspace not found'
106
+ });
107
+ }
108
+ if (workspace.ownerId === ctx.session.user.id) {
109
+ return 'owner';
110
+ }
111
+ if (workspace.members.length > 0) {
112
+ return workspace.members[0].role;
113
+ }
114
+ throw new TRPCError({
115
+ code: 'FORBIDDEN',
116
+ message: 'Access denied to this workspace'
117
+ });
118
+ }),
119
+ /**
120
+ * Invite a new member to the workspace
121
+ */
122
+ inviteMember: authedProcedure
123
+ .input(z.object({
124
+ workspaceId: z.string(),
125
+ email: z.string().email(),
126
+ role: z.enum(['admin', 'member']).default('member'),
127
+ }))
128
+ .mutation(async ({ ctx, input }) => {
129
+ // Check if user is owner or admin of the workspace
130
+ const workspace = await ctx.db.workspace.findFirst({
131
+ where: {
132
+ id: input.workspaceId,
133
+ ownerId: ctx.session.user.id // Only owners can invite for now
134
+ }
135
+ });
136
+ if (!workspace) {
137
+ throw new TRPCError({
138
+ code: 'NOT_FOUND',
139
+ message: 'Workspace not found or insufficient permissions'
140
+ });
141
+ }
142
+ // Check if user is already a member
143
+ const existingMember = await ctx.db.user.findFirst({
144
+ where: {
145
+ email: input.email,
146
+ OR: [
147
+ { id: workspace.ownerId },
148
+ { workspaceMemberships: { some: { workspaceId: input.workspaceId } } }
149
+ ]
150
+ }
151
+ });
152
+ if (existingMember) {
153
+ throw new TRPCError({
154
+ code: 'BAD_REQUEST',
155
+ message: 'User is already a member of this workspace'
156
+ });
157
+ }
158
+ // Check if there's already a pending invitation
159
+ const existingInvitation = await ctx.db.workspaceInvitation.findFirst({
160
+ where: {
161
+ workspaceId: input.workspaceId,
162
+ email: input.email,
163
+ acceptedAt: null,
164
+ expiresAt: { gt: new Date() }
165
+ }
166
+ });
167
+ if (existingInvitation) {
168
+ throw new TRPCError({
169
+ code: 'BAD_REQUEST',
170
+ message: 'Invitation already sent to this email'
171
+ });
172
+ }
173
+ // Create invitation
174
+ const invitation = await ctx.db.workspaceInvitation.create({
175
+ data: {
176
+ workspaceId: input.workspaceId,
177
+ email: input.email,
178
+ role: input.role,
179
+ invitedById: ctx.session.user.id,
180
+ },
181
+ include: {
182
+ workspace: {
183
+ select: {
184
+ title: true,
185
+ owner: {
186
+ select: {
187
+ name: true,
188
+ email: true,
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ });
195
+ logger.info(`🎫 Invitation created for ${input.email} to workspace ${input.workspaceId} with role ${input.role}`, 'WORKSPACE', {
196
+ invitationId: invitation.id,
197
+ workspaceId: input.workspaceId,
198
+ email: input.email,
199
+ role: input.role,
200
+ invitedBy: ctx.session.user.id
201
+ });
202
+ // TODO: Send email notification here
203
+ // await sendInvitationEmail(invitation);
204
+ return {
205
+ invitationId: invitation.id,
206
+ token: invitation.token,
207
+ email: invitation.email,
208
+ role: invitation.role,
209
+ expiresAt: invitation.expiresAt,
210
+ workspaceTitle: invitation.workspace.title,
211
+ invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email,
212
+ };
213
+ }),
214
+ /**
215
+ * Accept an invitation (public endpoint)
216
+ */
217
+ acceptInvite: publicProcedure
218
+ .input(z.object({
219
+ token: z.string(),
220
+ }))
221
+ .mutation(async ({ ctx, input }) => {
222
+ // Find the invitation
223
+ const invitation = await ctx.db.workspaceInvitation.findFirst({
224
+ where: {
225
+ token: input.token,
226
+ acceptedAt: null,
227
+ expiresAt: { gt: new Date() }
228
+ },
229
+ include: {
230
+ workspace: {
231
+ select: {
232
+ id: true,
233
+ title: true,
234
+ owner: {
235
+ select: {
236
+ name: true,
237
+ email: true,
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ });
244
+ if (!invitation) {
245
+ throw new TRPCError({
246
+ code: 'NOT_FOUND',
247
+ message: 'Invalid or expired invitation'
248
+ });
249
+ }
250
+ // Check if user is authenticated
251
+ if (!ctx.session?.user) {
252
+ throw new TRPCError({
253
+ code: 'UNAUTHORIZED',
254
+ message: 'Please log in to accept this invitation'
255
+ });
256
+ }
257
+ // Check if the email matches the user's email
258
+ if (ctx.session.user.email !== invitation.email) {
259
+ throw new TRPCError({
260
+ code: 'BAD_REQUEST',
261
+ message: 'This invitation was sent to a different email address'
262
+ });
263
+ }
264
+ // Check if user is already a member
265
+ const isAlreadyMember = await ctx.db.workspace.findFirst({
266
+ where: {
267
+ id: invitation.workspaceId,
268
+ OR: [
269
+ { ownerId: ctx.session.user.id },
270
+ { members: { some: { userId: ctx.session.user.id } } }
271
+ ]
272
+ }
273
+ });
274
+ if (isAlreadyMember) {
275
+ // Mark invitation as accepted even if already a member
276
+ await ctx.db.workspaceInvitation.update({
277
+ where: { id: invitation.id },
278
+ data: { acceptedAt: new Date() }
279
+ });
280
+ throw new TRPCError({
281
+ code: 'BAD_REQUEST',
282
+ message: 'You are already a member of this workspace'
283
+ });
284
+ }
285
+ // Add user to workspace with proper role
286
+ await ctx.db.workspaceMember.create({
287
+ data: {
288
+ workspaceId: invitation.workspaceId,
289
+ userId: ctx.session.user.id,
290
+ role: invitation.role,
291
+ }
292
+ });
293
+ // Mark invitation as accepted
294
+ await ctx.db.workspaceInvitation.update({
295
+ where: { id: invitation.id },
296
+ data: { acceptedAt: new Date() }
297
+ });
298
+ logger.info(`✅ Invitation accepted by ${ctx.session.user.id} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
299
+ invitationId: invitation.id,
300
+ workspaceId: invitation.workspaceId,
301
+ userId: ctx.session.user.id,
302
+ email: invitation.email
303
+ });
304
+ return {
305
+ workspaceId: invitation.workspaceId,
306
+ workspaceTitle: invitation.workspace.title,
307
+ role: invitation.role,
308
+ ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
309
+ };
310
+ }),
311
+ /**
312
+ * Change a member's role (owner only)
313
+ */
314
+ changeMemberRole: authedProcedure
315
+ .input(z.object({
316
+ workspaceId: z.string(),
317
+ memberId: z.string(),
318
+ role: z.enum(['admin', 'member']),
319
+ }))
320
+ .mutation(async ({ ctx, input }) => {
321
+ // Check if user is owner of the workspace
322
+ const workspace = await ctx.db.workspace.findFirst({
323
+ where: {
324
+ id: input.workspaceId,
325
+ ownerId: ctx.session.user.id
326
+ }
327
+ });
328
+ if (!workspace) {
329
+ throw new TRPCError({
330
+ code: 'NOT_FOUND',
331
+ message: 'Workspace not found or insufficient permissions'
332
+ });
333
+ }
334
+ // Check if member exists and is not the owner
335
+ if (input.memberId === workspace.ownerId) {
336
+ throw new TRPCError({
337
+ code: 'BAD_REQUEST',
338
+ message: 'Cannot change owner role'
339
+ });
340
+ }
341
+ const member = await ctx.db.workspaceMember.findFirst({
342
+ where: {
343
+ workspaceId: input.workspaceId,
344
+ userId: input.memberId
345
+ }
346
+ });
347
+ if (!member) {
348
+ throw new TRPCError({
349
+ code: 'NOT_FOUND',
350
+ message: 'Member not found in this workspace'
351
+ });
352
+ }
353
+ // Update the member's role
354
+ const updatedMember = await ctx.db.workspaceMember.update({
355
+ where: { id: member.id },
356
+ data: { role: input.role },
357
+ include: {
358
+ user: {
359
+ select: {
360
+ id: true,
361
+ name: true,
362
+ email: true,
363
+ }
364
+ }
365
+ }
366
+ });
367
+ logger.info(`🔄 Member role changed for ${input.memberId} from ${member.role} to ${input.role} in workspace ${input.workspaceId}`, 'WORKSPACE', {
368
+ workspaceId: input.workspaceId,
369
+ memberId: input.memberId,
370
+ oldRole: member.role,
371
+ newRole: input.role,
372
+ changedBy: ctx.session.user.id
373
+ });
374
+ return {
375
+ memberId: input.memberId,
376
+ role: input.role,
377
+ memberName: updatedMember.user.name || updatedMember.user.email,
378
+ message: 'Role changed successfully'
379
+ };
380
+ }),
381
+ /**
382
+ * Remove a member from the workspace (owner only)
383
+ */
384
+ removeMember: authedProcedure
385
+ .input(z.object({
386
+ workspaceId: z.string(),
387
+ memberId: z.string(),
388
+ }))
389
+ .mutation(async ({ ctx, input }) => {
390
+ // Check if user is owner of the workspace
391
+ const workspace = await ctx.db.workspace.findFirst({
392
+ where: {
393
+ id: input.workspaceId,
394
+ ownerId: ctx.session.user.id
395
+ }
396
+ });
397
+ if (!workspace) {
398
+ throw new TRPCError({
399
+ code: 'NOT_FOUND',
400
+ message: 'Workspace not found or insufficient permissions'
401
+ });
402
+ }
403
+ // Check if trying to remove the owner
404
+ if (input.memberId === workspace.ownerId) {
405
+ throw new TRPCError({
406
+ code: 'BAD_REQUEST',
407
+ message: 'Cannot remove workspace owner'
408
+ });
409
+ }
410
+ // Check if member exists
411
+ const member = await ctx.db.workspaceMember.findFirst({
412
+ where: {
413
+ workspaceId: input.workspaceId,
414
+ userId: input.memberId
415
+ },
416
+ include: {
417
+ user: {
418
+ select: {
419
+ name: true,
420
+ email: true,
421
+ }
422
+ }
423
+ }
424
+ });
425
+ if (!member) {
426
+ throw new TRPCError({
427
+ code: 'NOT_FOUND',
428
+ message: 'Member not found in this workspace'
429
+ });
430
+ }
431
+ // Remove member from workspace
432
+ await ctx.db.workspaceMember.delete({
433
+ where: { id: member.id }
434
+ });
435
+ logger.info(`🗑️ Member ${input.memberId} removed from workspace ${input.workspaceId}`, 'WORKSPACE', {
436
+ workspaceId: input.workspaceId,
437
+ memberId: input.memberId,
438
+ removedBy: ctx.session.user.id
439
+ });
440
+ return {
441
+ memberId: input.memberId,
442
+ message: 'Member removed successfully'
443
+ };
444
+ }),
445
+ /**
446
+ * Get pending invitations for a workspace (owner only)
447
+ */
448
+ getPendingInvitations: authedProcedure
449
+ .input(z.object({
450
+ workspaceId: z.string(),
451
+ }))
452
+ .query(async ({ ctx, input }) => {
453
+ // Check if user is owner of the workspace
454
+ const workspace = await ctx.db.workspace.findFirst({
455
+ where: {
456
+ id: input.workspaceId,
457
+ ownerId: ctx.session.user.id
458
+ }
459
+ });
460
+ if (!workspace) {
461
+ throw new TRPCError({
462
+ code: 'NOT_FOUND',
463
+ message: 'Workspace not found or insufficient permissions'
464
+ });
465
+ }
466
+ const invitations = await ctx.db.workspaceInvitation.findMany({
467
+ where: {
468
+ workspaceId: input.workspaceId,
469
+ acceptedAt: null,
470
+ expiresAt: { gt: new Date() }
471
+ },
472
+ include: {
473
+ invitedBy: {
474
+ select: {
475
+ name: true,
476
+ email: true,
477
+ }
478
+ }
479
+ },
480
+ orderBy: { createdAt: 'desc' }
481
+ });
482
+ return invitations.map(invitation => ({
483
+ id: invitation.id,
484
+ email: invitation.email,
485
+ role: invitation.role,
486
+ token: invitation.token,
487
+ expiresAt: invitation.expiresAt,
488
+ createdAt: invitation.createdAt,
489
+ invitedByName: invitation.invitedBy.name || invitation.invitedBy.email,
490
+ }));
491
+ }),
492
+ /**
493
+ * Cancel a pending invitation (owner only)
494
+ */
495
+ cancelInvitation: authedProcedure
496
+ .input(z.object({
497
+ invitationId: z.string(),
498
+ }))
499
+ .mutation(async ({ ctx, input }) => {
500
+ // Check if user is owner of the workspace
501
+ const invitation = await ctx.db.workspaceInvitation.findFirst({
502
+ where: {
503
+ id: input.invitationId,
504
+ acceptedAt: null,
505
+ workspace: {
506
+ ownerId: ctx.session.user.id
507
+ }
508
+ }
509
+ });
510
+ if (!invitation) {
511
+ throw new TRPCError({
512
+ code: 'NOT_FOUND',
513
+ message: 'Invitation not found or insufficient permissions'
514
+ });
515
+ }
516
+ // Delete the invitation
517
+ await ctx.db.workspaceInvitation.delete({
518
+ where: { id: input.invitationId }
519
+ });
520
+ logger.info(`❌ Invitation cancelled for ${invitation.email} to workspace ${invitation.workspaceId}`, 'WORKSPACE', {
521
+ invitationId: input.invitationId,
522
+ workspaceId: invitation.workspaceId,
523
+ email: invitation.email,
524
+ cancelledBy: ctx.session.user.id
525
+ });
526
+ return {
527
+ invitationId: input.invitationId,
528
+ message: 'Invitation cancelled successfully'
529
+ };
530
+ }),
531
+ });