@chaaskit/server 0.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 (189) hide show
  1. package/dist/api/admin.js +438 -0
  2. package/dist/api/admin.js.map +1 -0
  3. package/dist/api/agents.js +21 -0
  4. package/dist/api/agents.js.map +1 -0
  5. package/dist/api/api-keys.js +122 -0
  6. package/dist/api/api-keys.js.map +1 -0
  7. package/dist/api/auth.js +399 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/chat.js +900 -0
  10. package/dist/api/chat.js.map +1 -0
  11. package/dist/api/config.js +91 -0
  12. package/dist/api/config.js.map +1 -0
  13. package/dist/api/documents.js +237 -0
  14. package/dist/api/documents.js.map +1 -0
  15. package/dist/api/export.js +107 -0
  16. package/dist/api/export.js.map +1 -0
  17. package/dist/api/health.js +25 -0
  18. package/dist/api/health.js.map +1 -0
  19. package/dist/api/mcp-server.js +84 -0
  20. package/dist/api/mcp-server.js.map +1 -0
  21. package/dist/api/mcp.js +400 -0
  22. package/dist/api/mcp.js.map +1 -0
  23. package/dist/api/mentions.js +94 -0
  24. package/dist/api/mentions.js.map +1 -0
  25. package/dist/api/oauth.js +366 -0
  26. package/dist/api/oauth.js.map +1 -0
  27. package/dist/api/payments.js +473 -0
  28. package/dist/api/payments.js.map +1 -0
  29. package/dist/api/projects.js +301 -0
  30. package/dist/api/projects.js.map +1 -0
  31. package/dist/api/scheduled-prompts.js +617 -0
  32. package/dist/api/scheduled-prompts.js.map +1 -0
  33. package/dist/api/search.js +85 -0
  34. package/dist/api/search.js.map +1 -0
  35. package/dist/api/share.js +188 -0
  36. package/dist/api/share.js.map +1 -0
  37. package/dist/api/slack.js +468 -0
  38. package/dist/api/slack.js.map +1 -0
  39. package/dist/api/teams.js +693 -0
  40. package/dist/api/teams.js.map +1 -0
  41. package/dist/api/templates.js +134 -0
  42. package/dist/api/templates.js.map +1 -0
  43. package/dist/api/threads.js +323 -0
  44. package/dist/api/threads.js.map +1 -0
  45. package/dist/api/upload.js +57 -0
  46. package/dist/api/upload.js.map +1 -0
  47. package/dist/api/user.js +111 -0
  48. package/dist/api/user.js.map +1 -0
  49. package/dist/api/v1/openai.js +245 -0
  50. package/dist/api/v1/openai.js.map +1 -0
  51. package/dist/app.js +168 -0
  52. package/dist/app.js.map +1 -0
  53. package/dist/bin/cli.js +57 -0
  54. package/dist/bin/cli.js.map +1 -0
  55. package/dist/commands/db-sync.js +108 -0
  56. package/dist/commands/db-sync.js.map +1 -0
  57. package/dist/config/loader.js +374 -0
  58. package/dist/config/loader.js.map +1 -0
  59. package/dist/documents/extractors.js +136 -0
  60. package/dist/documents/extractors.js.map +1 -0
  61. package/dist/extensions/glob.js +53 -0
  62. package/dist/extensions/glob.js.map +1 -0
  63. package/dist/extensions/loader.js +72 -0
  64. package/dist/extensions/loader.js.map +1 -0
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/loaders/index.js +75 -0
  68. package/dist/loaders/index.js.map +1 -0
  69. package/dist/mcp/client.js +551 -0
  70. package/dist/mcp/client.js.map +1 -0
  71. package/dist/mcp/server.js +335 -0
  72. package/dist/mcp/server.js.map +1 -0
  73. package/dist/middleware/apiKeyAuth.js +136 -0
  74. package/dist/middleware/apiKeyAuth.js.map +1 -0
  75. package/dist/middleware/auth.js +192 -0
  76. package/dist/middleware/auth.js.map +1 -0
  77. package/dist/middleware/errorHandler.js +41 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/mcpServerAuth.js +164 -0
  80. package/dist/middleware/mcpServerAuth.js.map +1 -0
  81. package/dist/middleware/requestLogger.js +9 -0
  82. package/dist/middleware/requestLogger.js.map +1 -0
  83. package/dist/middleware/team.js +132 -0
  84. package/dist/middleware/team.js.map +1 -0
  85. package/dist/oauth/server.js +410 -0
  86. package/dist/oauth/server.js.map +1 -0
  87. package/dist/queue/cli.js +93 -0
  88. package/dist/queue/cli.js.map +1 -0
  89. package/dist/queue/handlers/index.js +91 -0
  90. package/dist/queue/handlers/index.js.map +1 -0
  91. package/dist/queue/handlers/scheduled-prompt.js +270 -0
  92. package/dist/queue/handlers/scheduled-prompt.js.map +1 -0
  93. package/dist/queue/index.js +91 -0
  94. package/dist/queue/index.js.map +1 -0
  95. package/dist/queue/providers/memory.js +296 -0
  96. package/dist/queue/providers/memory.js.map +1 -0
  97. package/dist/queue/providers/sqs.js +275 -0
  98. package/dist/queue/providers/sqs.js.map +1 -0
  99. package/dist/queue/scheduler.js +355 -0
  100. package/dist/queue/scheduler.js.map +1 -0
  101. package/dist/queue/types.js +5 -0
  102. package/dist/queue/types.js.map +1 -0
  103. package/dist/queue/worker.js +230 -0
  104. package/dist/queue/worker.js.map +1 -0
  105. package/dist/registry/index.js +40 -0
  106. package/dist/registry/index.js.map +1 -0
  107. package/dist/server.js +207 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/services/agent.js +530 -0
  110. package/dist/services/agent.js.map +1 -0
  111. package/dist/services/agents.js +194 -0
  112. package/dist/services/agents.js.map +1 -0
  113. package/dist/services/documents.js +507 -0
  114. package/dist/services/documents.js.map +1 -0
  115. package/dist/services/email/index.js +91 -0
  116. package/dist/services/email/index.js.map +1 -0
  117. package/dist/services/email/providers/ses.js +97 -0
  118. package/dist/services/email/providers/ses.js.map +1 -0
  119. package/dist/services/email/templates.js +194 -0
  120. package/dist/services/email/templates.js.map +1 -0
  121. package/dist/services/email/types.js +5 -0
  122. package/dist/services/email/types.js.map +1 -0
  123. package/dist/services/encryption.js +69 -0
  124. package/dist/services/encryption.js.map +1 -0
  125. package/dist/services/oauth-discovery.js +226 -0
  126. package/dist/services/oauth-discovery.js.map +1 -0
  127. package/dist/services/pendingConfirmation.js +105 -0
  128. package/dist/services/pendingConfirmation.js.map +1 -0
  129. package/dist/services/scheduledPrompts.js +70 -0
  130. package/dist/services/scheduledPrompts.js.map +1 -0
  131. package/dist/services/slack/client.js +174 -0
  132. package/dist/services/slack/client.js.map +1 -0
  133. package/dist/services/slack/events.js +189 -0
  134. package/dist/services/slack/events.js.map +1 -0
  135. package/dist/services/slack/index.js +6 -0
  136. package/dist/services/slack/index.js.map +1 -0
  137. package/dist/services/slack/notifications.js +124 -0
  138. package/dist/services/slack/notifications.js.map +1 -0
  139. package/dist/services/slack/signature.js +74 -0
  140. package/dist/services/slack/signature.js.map +1 -0
  141. package/dist/services/slack/thread-context.js +191 -0
  142. package/dist/services/slack/thread-context.js.map +1 -0
  143. package/dist/services/toolConfirmation.js +55 -0
  144. package/dist/services/toolConfirmation.js.map +1 -0
  145. package/dist/services/usage.js +241 -0
  146. package/dist/services/usage.js.map +1 -0
  147. package/dist/ssr/build.js +90 -0
  148. package/dist/ssr/build.js.map +1 -0
  149. package/dist/ssr/components/SSRMessageList.js +120 -0
  150. package/dist/ssr/components/SSRMessageList.js.map +1 -0
  151. package/dist/ssr/entry.client.js +8 -0
  152. package/dist/ssr/entry.client.js.map +1 -0
  153. package/dist/ssr/entry.server.js +71 -0
  154. package/dist/ssr/entry.server.js.map +1 -0
  155. package/dist/ssr/handler.js +51 -0
  156. package/dist/ssr/handler.js.map +1 -0
  157. package/dist/ssr/root.js +184 -0
  158. package/dist/ssr/root.js.map +1 -0
  159. package/dist/ssr/routes/login.js +140 -0
  160. package/dist/ssr/routes/login.js.map +1 -0
  161. package/dist/ssr/routes/pricing.js +195 -0
  162. package/dist/ssr/routes/pricing.js.map +1 -0
  163. package/dist/ssr/routes/privacy.js +39 -0
  164. package/dist/ssr/routes/privacy.js.map +1 -0
  165. package/dist/ssr/routes/register.js +148 -0
  166. package/dist/ssr/routes/register.js.map +1 -0
  167. package/dist/ssr/routes/shared.$shareId.js +153 -0
  168. package/dist/ssr/routes/shared.$shareId.js.map +1 -0
  169. package/dist/ssr/routes/terms.js +39 -0
  170. package/dist/ssr/routes/terms.js.map +1 -0
  171. package/dist/storage/index.js +43 -0
  172. package/dist/storage/index.js.map +1 -0
  173. package/dist/storage/providers/database.js +38 -0
  174. package/dist/storage/providers/database.js.map +1 -0
  175. package/dist/storage/providers/filesystem.js +51 -0
  176. package/dist/storage/providers/filesystem.js.map +1 -0
  177. package/dist/storage/types.js +2 -0
  178. package/dist/storage/types.js.map +1 -0
  179. package/dist/tools/documents.js +336 -0
  180. package/dist/tools/documents.js.map +1 -0
  181. package/dist/tools/get-plan-usage.js +82 -0
  182. package/dist/tools/get-plan-usage.js.map +1 -0
  183. package/dist/tools/index.js +106 -0
  184. package/dist/tools/index.js.map +1 -0
  185. package/dist/tools/types.js +2 -0
  186. package/dist/tools/types.js.map +1 -0
  187. package/dist/tools/web-scrape.js +145 -0
  188. package/dist/tools/web-scrape.js.map +1 -0
  189. package/package.json +93 -0
@@ -0,0 +1,693 @@
1
+ import { Router } from 'express';
2
+ import { db } from '@chaaskit/db';
3
+ import crypto from 'crypto';
4
+ import { HTTP_STATUS, createTeamSchema, updateTeamSchema, inviteMemberSchema, updateMemberRoleSchema, } from '@chaaskit/shared';
5
+ import { requireAuth } from '../middleware/auth.js';
6
+ import { requireTeamRole } from '../middleware/team.js';
7
+ import { AppError } from '../middleware/errorHandler.js';
8
+ import { getAgentById } from '../services/agents.js';
9
+ import { getConfig } from '../config/loader.js';
10
+ import { notifyTeamMemberJoined } from '../services/slack/notifications.js';
11
+ import { isEmailEnabled, sendEmail, generateTeamInviteEmailHtml, generateTeamInviteEmailText, } from '../services/email/index.js';
12
+ export const teamsRouter = Router();
13
+ // Middleware to check if teams feature is enabled
14
+ const requireTeamsEnabled = (req, res, next) => {
15
+ const config = getConfig();
16
+ if (!config.teams?.enabled) {
17
+ return next(new AppError(HTTP_STATUS.FORBIDDEN, 'Team workspaces are not enabled'));
18
+ }
19
+ next();
20
+ };
21
+ // Apply teams enabled check to all routes
22
+ teamsRouter.use(requireTeamsEnabled);
23
+ // List user's teams
24
+ teamsRouter.get('/', requireAuth, async (req, res, next) => {
25
+ try {
26
+ const memberships = await db.teamMember.findMany({
27
+ where: { userId: req.user.id },
28
+ include: {
29
+ team: {
30
+ include: {
31
+ _count: {
32
+ select: {
33
+ members: true,
34
+ threads: true,
35
+ },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ });
41
+ const teams = memberships
42
+ .filter((m) => !m.team.archivedAt) // Exclude archived teams
43
+ .map((m) => ({
44
+ id: m.team.id,
45
+ name: m.team.name,
46
+ archivedAt: m.team.archivedAt,
47
+ createdAt: m.team.createdAt,
48
+ updatedAt: m.team.updatedAt,
49
+ role: m.role,
50
+ memberCount: m.team._count.members,
51
+ threadCount: m.team._count.threads,
52
+ }));
53
+ res.json({ teams });
54
+ }
55
+ catch (error) {
56
+ next(error);
57
+ }
58
+ });
59
+ // Create new team
60
+ teamsRouter.post('/', requireAuth, async (req, res, next) => {
61
+ try {
62
+ const { name } = createTeamSchema.parse(req.body);
63
+ const team = await db.team.create({
64
+ data: {
65
+ name,
66
+ members: {
67
+ create: {
68
+ userId: req.user.id,
69
+ role: 'owner',
70
+ },
71
+ },
72
+ },
73
+ include: {
74
+ members: {
75
+ include: {
76
+ user: {
77
+ select: {
78
+ id: true,
79
+ email: true,
80
+ name: true,
81
+ avatarUrl: true,
82
+ },
83
+ },
84
+ },
85
+ },
86
+ invites: true,
87
+ },
88
+ });
89
+ const teamDetails = {
90
+ id: team.id,
91
+ name: team.name,
92
+ context: team.context,
93
+ archivedAt: team.archivedAt,
94
+ createdAt: team.createdAt,
95
+ updatedAt: team.updatedAt,
96
+ members: team.members.map((m) => ({
97
+ id: m.id,
98
+ teamId: m.teamId,
99
+ userId: m.userId,
100
+ role: m.role,
101
+ createdAt: m.createdAt,
102
+ user: m.user,
103
+ })),
104
+ invites: team.invites,
105
+ };
106
+ res.status(HTTP_STATUS.CREATED).json({ team: teamDetails });
107
+ }
108
+ catch (error) {
109
+ next(error);
110
+ }
111
+ });
112
+ // Get team details
113
+ teamsRouter.get('/:teamId', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
114
+ try {
115
+ const { teamId } = req.params;
116
+ const team = await db.team.findUnique({
117
+ where: { id: teamId },
118
+ include: {
119
+ members: {
120
+ include: {
121
+ user: {
122
+ select: {
123
+ id: true,
124
+ email: true,
125
+ name: true,
126
+ avatarUrl: true,
127
+ },
128
+ },
129
+ },
130
+ },
131
+ invites: {
132
+ where: {
133
+ acceptedAt: null,
134
+ expiresAt: { gt: new Date() },
135
+ },
136
+ },
137
+ },
138
+ });
139
+ if (!team) {
140
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Team not found');
141
+ }
142
+ const teamDetails = {
143
+ id: team.id,
144
+ name: team.name,
145
+ context: team.context,
146
+ archivedAt: team.archivedAt,
147
+ createdAt: team.createdAt,
148
+ updatedAt: team.updatedAt,
149
+ members: team.members.map((m) => ({
150
+ id: m.id,
151
+ teamId: m.teamId,
152
+ userId: m.userId,
153
+ role: m.role,
154
+ createdAt: m.createdAt,
155
+ user: m.user,
156
+ })),
157
+ invites: team.invites,
158
+ };
159
+ res.json({ team: teamDetails });
160
+ }
161
+ catch (error) {
162
+ next(error);
163
+ }
164
+ });
165
+ // Update team
166
+ teamsRouter.patch('/:teamId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
167
+ try {
168
+ const { teamId } = req.params;
169
+ const { name, context } = updateTeamSchema.parse(req.body);
170
+ const updateData = {};
171
+ if (name !== undefined)
172
+ updateData.name = name;
173
+ if (context !== undefined)
174
+ updateData.context = context;
175
+ const team = await db.team.update({
176
+ where: { id: teamId },
177
+ data: updateData,
178
+ });
179
+ res.json({ team });
180
+ }
181
+ catch (error) {
182
+ next(error);
183
+ }
184
+ });
185
+ // Archive team (owner only)
186
+ teamsRouter.post('/:teamId/archive', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
187
+ try {
188
+ const { teamId } = req.params;
189
+ const team = await db.team.update({
190
+ where: { id: teamId },
191
+ data: { archivedAt: new Date() },
192
+ });
193
+ res.json({ team });
194
+ }
195
+ catch (error) {
196
+ next(error);
197
+ }
198
+ });
199
+ // Unarchive team (owner only)
200
+ teamsRouter.post('/:teamId/unarchive', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
201
+ try {
202
+ const { teamId } = req.params;
203
+ // Need to bypass the archived check in middleware for unarchiving
204
+ const team = await db.team.findUnique({
205
+ where: { id: teamId },
206
+ });
207
+ if (!team) {
208
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Team not found');
209
+ }
210
+ const updatedTeam = await db.team.update({
211
+ where: { id: teamId },
212
+ data: { archivedAt: null },
213
+ });
214
+ res.json({ team: updatedTeam });
215
+ }
216
+ catch (error) {
217
+ next(error);
218
+ }
219
+ });
220
+ // Invite member (admin+)
221
+ teamsRouter.post('/:teamId/invite', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
222
+ try {
223
+ const { teamId } = req.params;
224
+ const { email, role } = inviteMemberSchema.parse(req.body);
225
+ // Check if user is already a member
226
+ const existingMember = await db.teamMember.findFirst({
227
+ where: {
228
+ teamId,
229
+ user: { email },
230
+ },
231
+ });
232
+ if (existingMember) {
233
+ throw new AppError(HTTP_STATUS.CONFLICT, 'User is already a member of this team');
234
+ }
235
+ // Check if there's already a pending invite
236
+ const existingInvite = await db.teamInvite.findUnique({
237
+ where: {
238
+ teamId_email: { teamId, email },
239
+ },
240
+ });
241
+ if (existingInvite && !existingInvite.acceptedAt && existingInvite.expiresAt > new Date()) {
242
+ throw new AppError(HTTP_STATUS.CONFLICT, 'An invite already exists for this email');
243
+ }
244
+ // Delete expired or used invites for this email
245
+ if (existingInvite) {
246
+ await db.teamInvite.delete({
247
+ where: { id: existingInvite.id },
248
+ });
249
+ }
250
+ const token = crypto.randomBytes(32).toString('hex');
251
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
252
+ const invite = await db.teamInvite.create({
253
+ data: {
254
+ teamId,
255
+ email,
256
+ role,
257
+ token,
258
+ invitedBy: req.user.id,
259
+ expiresAt,
260
+ },
261
+ });
262
+ const config = getConfig();
263
+ const appUrl = process.env.APP_URL || 'http://localhost:5173';
264
+ const inviteUrl = `${appUrl}/invite/${token}`;
265
+ // Get team name for email
266
+ const team = await db.team.findUnique({
267
+ where: { id: teamId },
268
+ select: { name: true },
269
+ });
270
+ // Send invitation email if email is enabled
271
+ let emailSent = false;
272
+ if (isEmailEnabled() && team) {
273
+ const inviterName = req.user.name || null;
274
+ const html = generateTeamInviteEmailHtml(team.name, inviteUrl, inviterName, config);
275
+ const text = generateTeamInviteEmailText(team.name, inviteUrl, inviterName, config);
276
+ try {
277
+ const result = await sendEmail({
278
+ to: email,
279
+ subject: `You're invited to join ${team.name} on ${config.app.name}`,
280
+ html,
281
+ text,
282
+ });
283
+ emailSent = !!result;
284
+ if (result) {
285
+ console.log(`[Teams] Invitation email sent to ${email} (messageId: ${result.messageId})`);
286
+ }
287
+ }
288
+ catch (err) {
289
+ console.error(`[Teams] Failed to send invitation email to ${email}:`, err);
290
+ // Continue even if email fails - the invite is still created
291
+ }
292
+ }
293
+ else {
294
+ // Email disabled - log the invite URL for development
295
+ console.log(`[Teams] Email disabled - invitation URL for ${email}: ${inviteUrl}`);
296
+ }
297
+ res.status(HTTP_STATUS.CREATED).json({
298
+ invite: invite,
299
+ inviteUrl,
300
+ emailSent,
301
+ });
302
+ }
303
+ catch (error) {
304
+ next(error);
305
+ }
306
+ });
307
+ // Get invite details (public - for invite acceptance page)
308
+ teamsRouter.get('/invite/:token', async (req, res, next) => {
309
+ try {
310
+ const { token } = req.params;
311
+ const invite = await db.teamInvite.findUnique({
312
+ where: { token },
313
+ include: {
314
+ team: {
315
+ select: {
316
+ id: true,
317
+ name: true,
318
+ },
319
+ },
320
+ },
321
+ });
322
+ if (!invite) {
323
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
324
+ }
325
+ if (invite.acceptedAt) {
326
+ throw new AppError(HTTP_STATUS.GONE, 'Invite has already been used');
327
+ }
328
+ if (invite.expiresAt < new Date()) {
329
+ throw new AppError(HTTP_STATUS.GONE, 'Invite has expired');
330
+ }
331
+ res.json({
332
+ invite: {
333
+ email: invite.email,
334
+ role: invite.role,
335
+ teamName: invite.team.name,
336
+ expiresAt: invite.expiresAt,
337
+ },
338
+ });
339
+ }
340
+ catch (error) {
341
+ next(error);
342
+ }
343
+ });
344
+ // Accept invite
345
+ teamsRouter.post('/invite/:token/accept', requireAuth, async (req, res, next) => {
346
+ try {
347
+ const { token } = req.params;
348
+ const invite = await db.teamInvite.findUnique({
349
+ where: { token },
350
+ include: {
351
+ team: true,
352
+ },
353
+ });
354
+ if (!invite) {
355
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
356
+ }
357
+ if (invite.acceptedAt) {
358
+ throw new AppError(HTTP_STATUS.GONE, 'Invite has already been used');
359
+ }
360
+ if (invite.expiresAt < new Date()) {
361
+ throw new AppError(HTTP_STATUS.GONE, 'Invite has expired');
362
+ }
363
+ // Verify email matches (optional - could allow any authenticated user)
364
+ if (invite.email.toLowerCase() !== req.user.email.toLowerCase()) {
365
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'This invite is for a different email address');
366
+ }
367
+ // Check if already a member
368
+ const existingMember = await db.teamMember.findUnique({
369
+ where: {
370
+ teamId_userId: {
371
+ teamId: invite.teamId,
372
+ userId: req.user.id,
373
+ },
374
+ },
375
+ });
376
+ if (existingMember) {
377
+ throw new AppError(HTTP_STATUS.CONFLICT, 'You are already a member of this team');
378
+ }
379
+ // Create membership and mark invite as accepted
380
+ await db.$transaction([
381
+ db.teamMember.create({
382
+ data: {
383
+ teamId: invite.teamId,
384
+ userId: req.user.id,
385
+ role: invite.role,
386
+ },
387
+ }),
388
+ db.teamInvite.update({
389
+ where: { id: invite.id },
390
+ data: { acceptedAt: new Date() },
391
+ }),
392
+ ]);
393
+ // Send Slack notification for new team member
394
+ notifyTeamMemberJoined(invite.teamId, req.user.name, req.user.email, invite.team.name).catch(err => console.error('[Teams] Slack notification failed:', err));
395
+ res.json({
396
+ team: {
397
+ id: invite.team.id,
398
+ name: invite.team.name,
399
+ archivedAt: invite.team.archivedAt,
400
+ createdAt: invite.team.createdAt,
401
+ updatedAt: invite.team.updatedAt,
402
+ },
403
+ role: invite.role,
404
+ });
405
+ }
406
+ catch (error) {
407
+ next(error);
408
+ }
409
+ });
410
+ // Cancel/delete invite (admin+)
411
+ teamsRouter.delete('/:teamId/invite/:inviteId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
412
+ try {
413
+ const { teamId, inviteId } = req.params;
414
+ const invite = await db.teamInvite.findUnique({
415
+ where: { id: inviteId },
416
+ });
417
+ if (!invite || invite.teamId !== teamId) {
418
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Invite not found');
419
+ }
420
+ await db.teamInvite.delete({
421
+ where: { id: inviteId },
422
+ });
423
+ res.status(HTTP_STATUS.NO_CONTENT).send();
424
+ }
425
+ catch (error) {
426
+ next(error);
427
+ }
428
+ });
429
+ // Remove member (admin+)
430
+ teamsRouter.delete('/:teamId/members/:userId', requireAuth, requireTeamRole('admin'), async (req, res, next) => {
431
+ try {
432
+ const { teamId, userId } = req.params;
433
+ // Can't remove yourself via this endpoint
434
+ if (userId === req.user.id) {
435
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Use leave endpoint to remove yourself');
436
+ }
437
+ const membership = await db.teamMember.findUnique({
438
+ where: {
439
+ teamId_userId: { teamId, userId },
440
+ },
441
+ });
442
+ if (!membership) {
443
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Member not found');
444
+ }
445
+ // Can't remove owner
446
+ if (membership.role === 'owner') {
447
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Cannot remove the team owner');
448
+ }
449
+ // Admins can't remove other admins (only owner can)
450
+ if (membership.role === 'admin' && req.teamMember?.role !== 'owner') {
451
+ throw new AppError(HTTP_STATUS.FORBIDDEN, 'Only the owner can remove admins');
452
+ }
453
+ await db.teamMember.delete({
454
+ where: { id: membership.id },
455
+ });
456
+ res.status(HTTP_STATUS.NO_CONTENT).send();
457
+ }
458
+ catch (error) {
459
+ next(error);
460
+ }
461
+ });
462
+ // Update member role (owner only)
463
+ teamsRouter.patch('/:teamId/members/:userId', requireAuth, requireTeamRole('owner'), async (req, res, next) => {
464
+ try {
465
+ const { teamId, userId } = req.params;
466
+ const { role } = updateMemberRoleSchema.parse(req.body);
467
+ // Can't change your own role
468
+ if (userId === req.user.id) {
469
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Cannot change your own role');
470
+ }
471
+ const membership = await db.teamMember.findUnique({
472
+ where: {
473
+ teamId_userId: { teamId, userId },
474
+ },
475
+ });
476
+ if (!membership) {
477
+ throw new AppError(HTTP_STATUS.NOT_FOUND, 'Member not found');
478
+ }
479
+ // Can't make someone else an owner (would need ownership transfer)
480
+ if (role === 'owner') {
481
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Cannot assign owner role. Use ownership transfer instead.');
482
+ }
483
+ const updatedMembership = await db.teamMember.update({
484
+ where: { id: membership.id },
485
+ data: { role },
486
+ include: {
487
+ user: {
488
+ select: {
489
+ id: true,
490
+ email: true,
491
+ name: true,
492
+ avatarUrl: true,
493
+ },
494
+ },
495
+ },
496
+ });
497
+ res.json({
498
+ member: {
499
+ id: updatedMembership.id,
500
+ teamId: updatedMembership.teamId,
501
+ userId: updatedMembership.userId,
502
+ role: updatedMembership.role,
503
+ createdAt: updatedMembership.createdAt,
504
+ user: updatedMembership.user,
505
+ },
506
+ });
507
+ }
508
+ catch (error) {
509
+ next(error);
510
+ }
511
+ });
512
+ // Leave team
513
+ teamsRouter.post('/:teamId/leave', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
514
+ try {
515
+ const { teamId } = req.params;
516
+ // Owner can't leave without transferring ownership
517
+ if (req.teamMember?.role === 'owner') {
518
+ throw new AppError(HTTP_STATUS.BAD_REQUEST, 'Owner cannot leave. Transfer ownership first or archive the team.');
519
+ }
520
+ await db.teamMember.delete({
521
+ where: {
522
+ teamId_userId: {
523
+ teamId,
524
+ userId: req.user.id,
525
+ },
526
+ },
527
+ });
528
+ res.status(HTTP_STATUS.NO_CONTENT).send();
529
+ }
530
+ catch (error) {
531
+ next(error);
532
+ }
533
+ });
534
+ // Get team threads
535
+ teamsRouter.get('/:teamId/threads', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
536
+ try {
537
+ const { teamId } = req.params;
538
+ const threads = await db.thread.findMany({
539
+ where: { teamId },
540
+ select: {
541
+ id: true,
542
+ title: true,
543
+ agentId: true,
544
+ parentThreadId: true,
545
+ createdAt: true,
546
+ updatedAt: true,
547
+ _count: {
548
+ select: { messages: true },
549
+ },
550
+ messages: {
551
+ orderBy: { createdAt: 'desc' },
552
+ take: 1,
553
+ select: { content: true },
554
+ },
555
+ },
556
+ orderBy: { updatedAt: 'desc' },
557
+ });
558
+ const threadSummaries = threads.map((thread) => {
559
+ const agent = getAgentById(thread.agentId);
560
+ return {
561
+ id: thread.id,
562
+ title: thread.title,
563
+ agentId: thread.agentId,
564
+ agentName: agent?.name,
565
+ parentThreadId: thread.parentThreadId || undefined,
566
+ createdAt: thread.createdAt,
567
+ updatedAt: thread.updatedAt,
568
+ messageCount: thread._count.messages,
569
+ lastMessagePreview: thread.messages[0]?.content.slice(0, 100),
570
+ };
571
+ });
572
+ res.json({ threads: threadSummaries });
573
+ }
574
+ catch (error) {
575
+ next(error);
576
+ }
577
+ });
578
+ // Get team stats
579
+ teamsRouter.get('/:teamId/stats', requireAuth, requireTeamRole('viewer'), async (req, res, next) => {
580
+ try {
581
+ const { teamId } = req.params;
582
+ // Get the start of the current month
583
+ const now = new Date();
584
+ const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
585
+ // Run all queries in parallel for efficiency
586
+ const [totalThreads, totalMessages, messagesThisMonth, threadsThisMonth, memberCount, recentThreads, recentMembers,] = await Promise.all([
587
+ // Total threads
588
+ db.thread.count({
589
+ where: { teamId },
590
+ }),
591
+ // Total messages (in team threads)
592
+ db.message.count({
593
+ where: {
594
+ thread: { teamId },
595
+ },
596
+ }),
597
+ // Messages this month
598
+ db.message.count({
599
+ where: {
600
+ thread: { teamId },
601
+ createdAt: { gte: startOfMonth },
602
+ },
603
+ }),
604
+ // Threads this month
605
+ db.thread.count({
606
+ where: {
607
+ teamId,
608
+ createdAt: { gte: startOfMonth },
609
+ },
610
+ }),
611
+ // Member count
612
+ db.teamMember.count({
613
+ where: { teamId },
614
+ }),
615
+ // Recent threads (for activity)
616
+ db.thread.findMany({
617
+ where: { teamId },
618
+ orderBy: { createdAt: 'desc' },
619
+ take: 10,
620
+ select: {
621
+ id: true,
622
+ title: true,
623
+ createdAt: true,
624
+ userId: true,
625
+ user: {
626
+ select: {
627
+ id: true,
628
+ name: true,
629
+ email: true,
630
+ },
631
+ },
632
+ },
633
+ }),
634
+ // Recent member joins (for activity)
635
+ db.teamMember.findMany({
636
+ where: { teamId },
637
+ orderBy: { createdAt: 'desc' },
638
+ take: 10,
639
+ select: {
640
+ id: true,
641
+ createdAt: true,
642
+ user: {
643
+ select: {
644
+ id: true,
645
+ name: true,
646
+ email: true,
647
+ },
648
+ },
649
+ },
650
+ }),
651
+ ]);
652
+ const stats = {
653
+ totalThreads,
654
+ totalMessages,
655
+ messagesThisMonth,
656
+ threadsThisMonth,
657
+ memberCount,
658
+ };
659
+ // Combine and sort activity items (filter out any with null users)
660
+ const activityItems = [
661
+ ...recentThreads
662
+ .filter((thread) => thread.user !== null)
663
+ .map((thread) => ({
664
+ type: 'thread_created',
665
+ timestamp: thread.createdAt,
666
+ user: {
667
+ id: thread.user.id,
668
+ name: thread.user.name,
669
+ email: thread.user.email,
670
+ },
671
+ details: thread.title || 'Untitled thread',
672
+ })),
673
+ ...recentMembers.map((member) => ({
674
+ type: 'member_joined',
675
+ timestamp: member.createdAt,
676
+ user: {
677
+ id: member.user.id,
678
+ name: member.user.name,
679
+ email: member.user.email,
680
+ },
681
+ })),
682
+ ];
683
+ // Sort by timestamp descending and take the 10 most recent
684
+ const recentActivity = activityItems
685
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
686
+ .slice(0, 10);
687
+ res.json({ stats, recentActivity });
688
+ }
689
+ catch (error) {
690
+ next(error);
691
+ }
692
+ });
693
+ //# sourceMappingURL=teams.js.map