@goscribe/server 1.3.1 → 1.3.3
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.
- package/dist/context.d.ts +2 -1
- package/dist/lib/env.js +1 -1
- package/dist/lib/stripe.d.ts +2 -1
- package/dist/lib/subscription_service.js +1 -1
- package/dist/routers/_app.d.ts +3 -2
- package/dist/routers/auth.js +18 -16
- package/dist/routers/payment.d.ts +2 -2
- package/dist/routers/workspace.d.ts +1 -0
- package/dist/routers/workspace.js +2 -0
- package/dist/trpc.d.ts +4 -4
- package/package.json +4 -2
- package/src/lib/env.ts +1 -1
- package/src/lib/subscription_service.ts +1 -1
- package/src/routers/auth.ts +19 -16
- package/src/routers/payment.ts +2 -0
- package/src/routers/workspace.ts +2 -0
- package/dist/routers/meetingsummary.d.ts +0 -0
- package/dist/routers/meetingsummary.js +0 -377
package/dist/context.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
|
2
2
|
import { prisma } from "./lib/prisma.js";
|
|
3
|
+
import cookie from "cookie";
|
|
3
4
|
export declare function createContext({ req, res }: CreateExpressContextOptions): Promise<{
|
|
4
5
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
5
6
|
session: any;
|
|
6
7
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
7
8
|
res: import("express").Response<any, Record<string, any>>;
|
|
8
|
-
cookies:
|
|
9
|
+
cookies: cookie.Cookies;
|
|
9
10
|
}>;
|
|
10
11
|
/** Use `typeof prisma` for `db` so TS keeps generated model delegates (not a bare PrismaClient). */
|
|
11
12
|
export type Context = Omit<Awaited<ReturnType<typeof createContext>>, "db"> & {
|
package/dist/lib/env.js
CHANGED
|
@@ -33,7 +33,7 @@ const envSchema = z.object({
|
|
|
33
33
|
SMTP_USER: z.string().optional(),
|
|
34
34
|
SMTP_PASSWORD: z.string().optional(),
|
|
35
35
|
SMTP_SECURE: z.enum(['true', 'false']).default('false').transform((v) => v === 'true'),
|
|
36
|
-
EMAIL_FROM: z.string().default('Scribe <
|
|
36
|
+
EMAIL_FROM: z.string().default('Scribe <hello@scribe.study>'),
|
|
37
37
|
// Stripe
|
|
38
38
|
STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(),
|
|
39
39
|
STRIPE_SUCCESS_URL: z.string().url().default('http://localhost:3000/payment-success'),
|
package/dist/lib/stripe.d.ts
CHANGED
|
@@ -522,7 +522,7 @@ export async function getUserStorageLimit(userId) {
|
|
|
522
522
|
return Number(activeSub.plan.limit.maxStorageBytes);
|
|
523
523
|
}
|
|
524
524
|
// Default limit (fallback for users with no active subscription)
|
|
525
|
-
return
|
|
525
|
+
return 1024 * 1024 * 1024; // 1GB
|
|
526
526
|
}
|
|
527
527
|
/**
|
|
528
528
|
* Core logic to sync Stripe subscription state with Prisma database
|
package/dist/routers/_app.d.ts
CHANGED
|
@@ -268,6 +268,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
268
268
|
name: string;
|
|
269
269
|
description?: string | undefined;
|
|
270
270
|
parentId?: string | undefined;
|
|
271
|
+
markerColor?: string | null | undefined;
|
|
271
272
|
};
|
|
272
273
|
output: {
|
|
273
274
|
id: string;
|
|
@@ -2612,7 +2613,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
2612
2613
|
planId: string;
|
|
2613
2614
|
};
|
|
2614
2615
|
output: {
|
|
2615
|
-
url:
|
|
2616
|
+
url: string | null;
|
|
2616
2617
|
};
|
|
2617
2618
|
meta: object;
|
|
2618
2619
|
}>;
|
|
@@ -2641,7 +2642,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
2641
2642
|
quantity?: number | undefined;
|
|
2642
2643
|
};
|
|
2643
2644
|
output: {
|
|
2644
|
-
url:
|
|
2645
|
+
url: string | null;
|
|
2645
2646
|
};
|
|
2646
2647
|
meta: object;
|
|
2647
2648
|
}>;
|
package/dist/routers/auth.js
CHANGED
|
@@ -10,6 +10,17 @@ import { supabaseClient } from '../lib/storage.js';
|
|
|
10
10
|
import { sendVerificationEmail, sendAccountDeletionScheduledEmail, sendAccountRestoredEmail, sendPasswordResetEmail, } from '../lib/email.js';
|
|
11
11
|
import { createStripeCustomer } from '../lib/stripe.js';
|
|
12
12
|
import { notifyAdminsAccountDeletionScheduled, notifyAdminsOnSignup, } from '../lib/notification-service.js';
|
|
13
|
+
function getAuthCookieConfig(isProduction) {
|
|
14
|
+
return {
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
secure: isProduction,
|
|
17
|
+
sameSite: (isProduction ? 'none' : 'lax'),
|
|
18
|
+
path: '/',
|
|
19
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
20
|
+
// Use parent domain in production so scribe.study and api.scribe.study share auth state.
|
|
21
|
+
domain: isProduction ? '.scribe.study' : undefined,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
13
24
|
// Helper to create custom auth token
|
|
14
25
|
const passwordFieldSchema = z
|
|
15
26
|
.string()
|
|
@@ -269,14 +280,7 @@ export const auth = router({
|
|
|
269
280
|
// Create custom auth token
|
|
270
281
|
const authToken = createCustomAuthToken(user.id);
|
|
271
282
|
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER);
|
|
272
|
-
const cookieValue = serialize("auth_token", authToken,
|
|
273
|
-
httpOnly: true,
|
|
274
|
-
secure: isProduction, // true for production/HTTPS, false for localhost
|
|
275
|
-
sameSite: isProduction ? "none" : "lax", // none for cross-origin, lax for same-origin
|
|
276
|
-
path: "/",
|
|
277
|
-
domain: isProduction ? "server-w8mz.onrender.com" : undefined,
|
|
278
|
-
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
279
|
-
});
|
|
283
|
+
const cookieValue = serialize("auth_token", authToken, getAuthCookieConfig(isProduction));
|
|
280
284
|
ctx.res.setHeader("Set-Cookie", cookieValue);
|
|
281
285
|
return {
|
|
282
286
|
id: user.id,
|
|
@@ -425,11 +429,10 @@ export const auth = router({
|
|
|
425
429
|
sendAccountDeletionScheduledEmail(user.email, token).catch(() => { });
|
|
426
430
|
}
|
|
427
431
|
// Log out user by clearing cookie
|
|
432
|
+
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER);
|
|
433
|
+
const clearCookieConfig = getAuthCookieConfig(isProduction);
|
|
428
434
|
ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
|
|
429
|
-
|
|
430
|
-
secure: process.env.NODE_ENV === "production",
|
|
431
|
-
sameSite: "lax",
|
|
432
|
-
path: "/",
|
|
435
|
+
...clearCookieConfig,
|
|
433
436
|
maxAge: 0,
|
|
434
437
|
}));
|
|
435
438
|
return { success: true, message: 'Account scheduled for deletion' };
|
|
@@ -468,11 +471,10 @@ export const auth = router({
|
|
|
468
471
|
}
|
|
469
472
|
// We don't need to delete from db.session because we use a stateless
|
|
470
473
|
// custom HMAC auth system (auth_token cookie).
|
|
474
|
+
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER);
|
|
475
|
+
const clearCookieConfig = getAuthCookieConfig(isProduction);
|
|
471
476
|
ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
|
|
472
|
-
|
|
473
|
-
secure: process.env.NODE_ENV === "production",
|
|
474
|
-
sameSite: "lax",
|
|
475
|
-
path: "/",
|
|
477
|
+
...clearCookieConfig,
|
|
476
478
|
maxAge: 0, // Expire immediately
|
|
477
479
|
}));
|
|
478
480
|
return { success: true };
|
|
@@ -24,7 +24,7 @@ export declare const paymentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
24
24
|
planId: string;
|
|
25
25
|
};
|
|
26
26
|
output: {
|
|
27
|
-
url:
|
|
27
|
+
url: string | null;
|
|
28
28
|
};
|
|
29
29
|
meta: object;
|
|
30
30
|
}>;
|
|
@@ -53,7 +53,7 @@ export declare const paymentRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
53
53
|
quantity?: number | undefined;
|
|
54
54
|
};
|
|
55
55
|
output: {
|
|
56
|
-
url:
|
|
56
|
+
url: string | null;
|
|
57
57
|
};
|
|
58
58
|
meta: object;
|
|
59
59
|
}>;
|
|
@@ -153,6 +153,7 @@ export const workspace = router({
|
|
|
153
153
|
name: z.string().min(1).max(100),
|
|
154
154
|
description: z.string().max(500).optional(),
|
|
155
155
|
parentId: z.string().optional(),
|
|
156
|
+
markerColor: z.string().nullable().optional(),
|
|
156
157
|
}))
|
|
157
158
|
.mutation(async ({ ctx, input }) => {
|
|
158
159
|
const ws = await ctx.db.workspace.create({
|
|
@@ -161,6 +162,7 @@ export const workspace = router({
|
|
|
161
162
|
description: input.description,
|
|
162
163
|
ownerId: ctx.session.user.id,
|
|
163
164
|
folderId: input.parentId ?? null,
|
|
165
|
+
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
164
166
|
artifacts: {
|
|
165
167
|
create: {
|
|
166
168
|
type: ArtifactType.FLASHCARD_SET,
|
package/dist/trpc.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilde
|
|
|
24
24
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
25
25
|
res: import("express").Response<any, Record<string, any>>;
|
|
26
26
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
27
|
-
cookies:
|
|
27
|
+
cookies: import("cookie").Cookies;
|
|
28
28
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
29
29
|
export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
30
30
|
userId: any;
|
|
@@ -32,7 +32,7 @@ export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuil
|
|
|
32
32
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
33
33
|
res: import("express").Response<any, Record<string, any>>;
|
|
34
34
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
35
|
-
cookies:
|
|
35
|
+
cookies: import("cookie").Cookies;
|
|
36
36
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
37
37
|
export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
38
38
|
userId: any;
|
|
@@ -40,7 +40,7 @@ export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder
|
|
|
40
40
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
41
41
|
res: import("express").Response<any, Record<string, any>>;
|
|
42
42
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
43
|
-
cookies:
|
|
43
|
+
cookies: import("cookie").Cookies;
|
|
44
44
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
45
45
|
export declare const limitedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
46
46
|
userId: any;
|
|
@@ -48,5 +48,5 @@ export declare const limitedProcedure: import("@trpc/server").TRPCProcedureBuild
|
|
|
48
48
|
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
49
49
|
res: import("express").Response<any, Record<string, any>>;
|
|
50
50
|
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
51
|
-
cookies:
|
|
51
|
+
cookies: import("cookie").Cookies;
|
|
52
52
|
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goscribe/server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"build": "npx prisma generate && tsc -p .",
|
|
17
17
|
"start": "node --experimental-specifier-resolution=node dist/server.js",
|
|
18
18
|
"test": "tsx --test src/lib/worksheet-generation.test.ts",
|
|
19
|
-
"test:activity": "tsx --test src/lib/activity_log_service.test.ts src/lib/activity_human_description.test.ts"
|
|
19
|
+
"test:activity": "tsx --test src/lib/activity_log_service.test.ts src/lib/activity_human_description.test.ts",
|
|
20
|
+
"generate": "npx prisma generate",
|
|
21
|
+
"prepublishOnly": "npm run generate && npm run build"
|
|
20
22
|
},
|
|
21
23
|
"prisma": {
|
|
22
24
|
"seed": "node --experimental-specifier-resolution=node prisma/seed.mjs"
|
package/src/lib/env.ts
CHANGED
|
@@ -42,7 +42,7 @@ const envSchema = z.object({
|
|
|
42
42
|
SMTP_USER: z.string().optional(),
|
|
43
43
|
SMTP_PASSWORD: z.string().optional(),
|
|
44
44
|
SMTP_SECURE: z.enum(['true', 'false']).default('false').transform((v) => v === 'true'),
|
|
45
|
-
EMAIL_FROM: z.string().default('Scribe <
|
|
45
|
+
EMAIL_FROM: z.string().default('Scribe <hello@scribe.study>'),
|
|
46
46
|
// Stripe
|
|
47
47
|
STRIPE_SECRET_KEY: z.string().startsWith('sk_').optional(),
|
|
48
48
|
STRIPE_SUCCESS_URL: z.string().url().default('http://localhost:3000/payment-success'),
|
package/src/routers/auth.ts
CHANGED
|
@@ -19,6 +19,18 @@ import {
|
|
|
19
19
|
notifyAdminsOnSignup,
|
|
20
20
|
} from '../lib/notification-service.js';
|
|
21
21
|
|
|
22
|
+
function getAuthCookieConfig(isProduction: boolean) {
|
|
23
|
+
return {
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
secure: isProduction,
|
|
26
|
+
sameSite: (isProduction ? 'none' : 'lax') as 'none' | 'lax',
|
|
27
|
+
path: '/',
|
|
28
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
29
|
+
// Use parent domain in production so scribe.study and api.scribe.study share auth state.
|
|
30
|
+
domain: isProduction ? '.scribe.study' : undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
// Helper to create custom auth token
|
|
23
35
|
const passwordFieldSchema = z
|
|
24
36
|
.string()
|
|
@@ -322,14 +334,7 @@ export const auth = router({
|
|
|
322
334
|
|
|
323
335
|
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER) as boolean;
|
|
324
336
|
|
|
325
|
-
const cookieValue = serialize("auth_token", authToken,
|
|
326
|
-
httpOnly: true,
|
|
327
|
-
secure: isProduction, // true for production/HTTPS, false for localhost
|
|
328
|
-
sameSite: isProduction ? "none" : "lax", // none for cross-origin, lax for same-origin
|
|
329
|
-
path: "/",
|
|
330
|
-
domain: isProduction ? "server-w8mz.onrender.com" : undefined,
|
|
331
|
-
maxAge: 60 * 60 * 24 * 30, // 30 days
|
|
332
|
-
});
|
|
337
|
+
const cookieValue = serialize("auth_token", authToken, getAuthCookieConfig(isProduction));
|
|
333
338
|
|
|
334
339
|
ctx.res.setHeader("Set-Cookie", cookieValue);
|
|
335
340
|
|
|
@@ -513,11 +518,10 @@ export const auth = router({
|
|
|
513
518
|
}
|
|
514
519
|
|
|
515
520
|
// Log out user by clearing cookie
|
|
521
|
+
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER) as boolean;
|
|
522
|
+
const clearCookieConfig = getAuthCookieConfig(isProduction);
|
|
516
523
|
ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
|
|
517
|
-
|
|
518
|
-
secure: process.env.NODE_ENV === "production",
|
|
519
|
-
sameSite: "lax",
|
|
520
|
-
path: "/",
|
|
524
|
+
...clearCookieConfig,
|
|
521
525
|
maxAge: 0,
|
|
522
526
|
}));
|
|
523
527
|
|
|
@@ -570,11 +574,10 @@ export const auth = router({
|
|
|
570
574
|
// custom HMAC auth system (auth_token cookie).
|
|
571
575
|
|
|
572
576
|
|
|
577
|
+
const isProduction = (process.env.NODE_ENV === "production" || process.env.RENDER) as boolean;
|
|
578
|
+
const clearCookieConfig = getAuthCookieConfig(isProduction);
|
|
573
579
|
ctx.res.setHeader("Set-Cookie", serialize("auth_token", "", {
|
|
574
|
-
|
|
575
|
-
secure: process.env.NODE_ENV === "production",
|
|
576
|
-
sameSite: "lax",
|
|
577
|
-
path: "/",
|
|
580
|
+
...clearCookieConfig,
|
|
578
581
|
maxAge: 0, // Expire immediately
|
|
579
582
|
}));
|
|
580
583
|
|
package/src/routers/payment.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from '../lib/notification-service.js';
|
|
12
12
|
import { upsertSubscriptionFromStripe } from '../lib/subscription_service.js';
|
|
13
13
|
import { ArtifactType } from '../lib/prisma.js';
|
|
14
|
+
import { PrismaClient } from '@prisma/client';
|
|
15
|
+
import { Stripe } from 'stripe';
|
|
14
16
|
|
|
15
17
|
const ArtifactTypeUnion = z.enum(['STUDY_GUIDE', 'FLASHCARD_SET', 'WORKSHEET', 'MEETING_SUMMARY', 'PODCAST_EPISODE', 'STORAGE']);
|
|
16
18
|
|
package/src/routers/workspace.ts
CHANGED
|
@@ -193,6 +193,7 @@ export const workspace = router({
|
|
|
193
193
|
name: z.string().min(1).max(100),
|
|
194
194
|
description: z.string().max(500).optional(),
|
|
195
195
|
parentId: z.string().optional(),
|
|
196
|
+
markerColor: z.string().nullable().optional(),
|
|
196
197
|
}))
|
|
197
198
|
.mutation(async ({ ctx, input }) => {
|
|
198
199
|
const ws = await ctx.db.workspace.create({
|
|
@@ -201,6 +202,7 @@ export const workspace = router({
|
|
|
201
202
|
description: input.description,
|
|
202
203
|
ownerId: ctx.session.user.id,
|
|
203
204
|
folderId: input.parentId ?? null,
|
|
205
|
+
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
204
206
|
artifacts: {
|
|
205
207
|
create: {
|
|
206
208
|
type: ArtifactType.FLASHCARD_SET,
|
|
File without changes
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// import { z } from 'zod';
|
|
3
|
-
// import { TRPCError } from '@trpc/server';
|
|
4
|
-
// import { router, authedProcedure } from '../trpc.js';
|
|
5
|
-
// import OpenAI from 'openai';
|
|
6
|
-
// import fs from 'fs';
|
|
7
|
-
// import path from 'path';
|
|
8
|
-
// import { v4 as uuidv4 } from 'uuid';
|
|
9
|
-
// // Prisma enum values mapped manually to avoid type import issues in ESM
|
|
10
|
-
// const ArtifactType = {
|
|
11
|
-
// STUDY_GUIDE: 'STUDY_GUIDE',
|
|
12
|
-
// FLASHCARD_SET: 'FLASHCARD_SET',
|
|
13
|
-
// WORKSHEET: 'WORKSHEET',
|
|
14
|
-
// MEETING_SUMMARY: 'MEETING_SUMMARY',
|
|
15
|
-
// PODCAST_EPISODE: 'PODCAST_EPISODE',
|
|
16
|
-
// } as const;
|
|
17
|
-
// // Initialize OpenAI client
|
|
18
|
-
// const openai = new OpenAI({
|
|
19
|
-
// apiKey: process.env.OPENAI_API_KEY,
|
|
20
|
-
// });
|
|
21
|
-
// // Meeting summary schema for structured data
|
|
22
|
-
// const meetingSchema = z.object({
|
|
23
|
-
// title: z.string(),
|
|
24
|
-
// participants: z.array(z.string()),
|
|
25
|
-
// date: z.string(),
|
|
26
|
-
// duration: z.string().optional(),
|
|
27
|
-
// agenda: z.array(z.string()).optional(),
|
|
28
|
-
// transcript: z.string().optional(),
|
|
29
|
-
// notes: z.string().optional(),
|
|
30
|
-
// });
|
|
31
|
-
// export const meetingSummarize = router({
|
|
32
|
-
// // List all meeting summaries for a workspace
|
|
33
|
-
// listSummaries: authedProcedure
|
|
34
|
-
// .input(z.object({ workspaceId: z.string() }))
|
|
35
|
-
// .query(async ({ ctx, input }) => {
|
|
36
|
-
// const workspace = await ctx.db.workspace.findFirst({
|
|
37
|
-
// where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
38
|
-
// });
|
|
39
|
-
// if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
40
|
-
// return ctx.db.artifact.findMany({
|
|
41
|
-
// where: {
|
|
42
|
-
// workspaceId: input.workspaceId,
|
|
43
|
-
// type: ArtifactType.MEETING_SUMMARY
|
|
44
|
-
// },
|
|
45
|
-
// include: {
|
|
46
|
-
// versions: {
|
|
47
|
-
// orderBy: { version: 'desc' },
|
|
48
|
-
// take: 1, // Get only the latest version
|
|
49
|
-
// },
|
|
50
|
-
// },
|
|
51
|
-
// orderBy: { updatedAt: 'desc' },
|
|
52
|
-
// });
|
|
53
|
-
// }),
|
|
54
|
-
// // Get a specific meeting summary
|
|
55
|
-
// getSummary: authedProcedure
|
|
56
|
-
// .input(z.object({ summaryId: z.string() }))
|
|
57
|
-
// .query(async ({ ctx, input }) => {
|
|
58
|
-
// const summary = await ctx.db.artifact.findFirst({
|
|
59
|
-
// where: {
|
|
60
|
-
// id: input.summaryId,
|
|
61
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
62
|
-
// workspace: { ownerId: ctx.session.user.id }
|
|
63
|
-
// },
|
|
64
|
-
// include: {
|
|
65
|
-
// versions: {
|
|
66
|
-
// orderBy: { version: 'desc' },
|
|
67
|
-
// take: 1,
|
|
68
|
-
// },
|
|
69
|
-
// },
|
|
70
|
-
// });
|
|
71
|
-
// if (!summary) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
72
|
-
// return summary;
|
|
73
|
-
// }),
|
|
74
|
-
// // Upload and process audio/video file
|
|
75
|
-
// uploadFile: authedProcedure
|
|
76
|
-
// .input(z.object({
|
|
77
|
-
// workspaceId: z.string(),
|
|
78
|
-
// fileName: z.string(),
|
|
79
|
-
// fileBuffer: z.string(), // Base64 encoded file
|
|
80
|
-
// mimeType: z.string(),
|
|
81
|
-
// title: z.string().optional(),
|
|
82
|
-
// }))
|
|
83
|
-
// .mutation(async ({ ctx, input }) => {
|
|
84
|
-
// const workspace = await ctx.db.workspace.findFirst({
|
|
85
|
-
// where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
86
|
-
// });
|
|
87
|
-
// if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
88
|
-
// // Validate file type
|
|
89
|
-
// const allowedTypes = ['audio/mpeg', 'audio/mp3', 'video/mp4', 'audio/wav', 'audio/m4a'];
|
|
90
|
-
// if (!allowedTypes.includes(input.mimeType)) {
|
|
91
|
-
// throw new TRPCError({
|
|
92
|
-
// code: 'BAD_REQUEST',
|
|
93
|
-
// message: 'Unsupported file type. Please upload MP3, MP4, WAV, or M4A files.'
|
|
94
|
-
// });
|
|
95
|
-
// }
|
|
96
|
-
// try {
|
|
97
|
-
// // Create temporary file
|
|
98
|
-
// const tempDir = path.join(process.cwd(), 'temp');
|
|
99
|
-
// if (!fs.existsSync(tempDir)) {
|
|
100
|
-
// fs.mkdirSync(tempDir, { recursive: true });
|
|
101
|
-
// }
|
|
102
|
-
// const fileId = uuidv4();
|
|
103
|
-
// const fileExtension = path.extname(input.fileName);
|
|
104
|
-
// const tempFilePath = path.join(tempDir, `${fileId}${fileExtension}`);
|
|
105
|
-
// // Write buffer to temporary file
|
|
106
|
-
// const fileBuffer = Buffer.from(input.fileBuffer, 'base64');
|
|
107
|
-
// fs.writeFileSync(tempFilePath, fileBuffer);
|
|
108
|
-
// // Transcribe audio using OpenAI Whisper
|
|
109
|
-
// const transcript = await openai.audio.transcriptions.create({
|
|
110
|
-
// file: fs.createReadStream(tempFilePath),
|
|
111
|
-
// model: 'whisper-1',
|
|
112
|
-
// response_format: 'text',
|
|
113
|
-
// });
|
|
114
|
-
// // Generate meeting summary using GPT
|
|
115
|
-
// const summaryResponse = await openai.chat.completions.create({
|
|
116
|
-
// model: 'gpt-4-turbo-preview',
|
|
117
|
-
// messages: [
|
|
118
|
-
// {
|
|
119
|
-
// role: 'system',
|
|
120
|
-
// content: `You are a meeting summarizer. Given a meeting transcript, create a comprehensive summary that includes:
|
|
121
|
-
// 1. Meeting title/topic
|
|
122
|
-
// 2. Key participants mentioned
|
|
123
|
-
// 3. Main discussion points
|
|
124
|
-
// 4. Action items
|
|
125
|
-
// 5. Decisions made
|
|
126
|
-
// 6. Next steps
|
|
127
|
-
// Format the response as JSON with the following structure:
|
|
128
|
-
// {
|
|
129
|
-
// "title": "Meeting title",
|
|
130
|
-
// "participants": ["participant1", "participant2"],
|
|
131
|
-
// "keyPoints": ["point1", "point2"],
|
|
132
|
-
// "actionItems": ["action1", "action2"],
|
|
133
|
-
// "decisions": ["decision1", "decision2"],
|
|
134
|
-
// "nextSteps": ["step1", "step2"],
|
|
135
|
-
// "summary": "Overall meeting summary"
|
|
136
|
-
// }`
|
|
137
|
-
// },
|
|
138
|
-
// {
|
|
139
|
-
// role: 'user',
|
|
140
|
-
// content: `Please summarize this meeting transcript: ${transcript}`
|
|
141
|
-
// }
|
|
142
|
-
// ],
|
|
143
|
-
// });
|
|
144
|
-
// let summaryData;
|
|
145
|
-
// try {
|
|
146
|
-
// summaryData = JSON.parse(summaryResponse.choices[0]?.message?.content || '{}');
|
|
147
|
-
// } catch (parseError) {
|
|
148
|
-
// // Fallback if JSON parsing fails
|
|
149
|
-
// summaryData = {
|
|
150
|
-
// title: input.title || 'Meeting Summary',
|
|
151
|
-
// participants: [],
|
|
152
|
-
// keyPoints: [],
|
|
153
|
-
// actionItems: [],
|
|
154
|
-
// decisions: [],
|
|
155
|
-
// nextSteps: [],
|
|
156
|
-
// summary: summaryResponse.choices[0]?.message?.content || 'Unable to generate summary'
|
|
157
|
-
// };
|
|
158
|
-
// }
|
|
159
|
-
// // Create artifact in database
|
|
160
|
-
// const artifact = await ctx.db.artifact.create({
|
|
161
|
-
// data: {
|
|
162
|
-
// workspaceId: input.workspaceId,
|
|
163
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
164
|
-
// title: summaryData.title || input.title || 'Meeting Summary',
|
|
165
|
-
// content: JSON.stringify({
|
|
166
|
-
// originalFileName: input.fileName,
|
|
167
|
-
// transcript: transcript,
|
|
168
|
-
// ...summaryData
|
|
169
|
-
// }),
|
|
170
|
-
// },
|
|
171
|
-
// });
|
|
172
|
-
// // Create initial version
|
|
173
|
-
// await ctx.db.artifactVersion.create({
|
|
174
|
-
// data: {
|
|
175
|
-
// artifactId: artifact.id,
|
|
176
|
-
// version: 1,
|
|
177
|
-
// content: artifact.content,
|
|
178
|
-
// },
|
|
179
|
-
// });
|
|
180
|
-
// // Clean up temporary file
|
|
181
|
-
// fs.unlinkSync(tempFilePath);
|
|
182
|
-
// return {
|
|
183
|
-
// id: artifact.id,
|
|
184
|
-
// title: artifact.title,
|
|
185
|
-
// summary: summaryData,
|
|
186
|
-
// transcript: transcript,
|
|
187
|
-
// };
|
|
188
|
-
// } catch (error) {
|
|
189
|
-
// console.error('Error processing meeting file:', error);
|
|
190
|
-
// throw new TRPCError({
|
|
191
|
-
// code: 'INTERNAL_SERVER_ERROR',
|
|
192
|
-
// message: 'Failed to process meeting file'
|
|
193
|
-
// });
|
|
194
|
-
// }
|
|
195
|
-
// }),
|
|
196
|
-
// // Process meeting data from schema
|
|
197
|
-
// processSchema: authedProcedure
|
|
198
|
-
// .input(z.object({
|
|
199
|
-
// workspaceId: z.string(),
|
|
200
|
-
// meetingData: meetingSchema,
|
|
201
|
-
// }))
|
|
202
|
-
// .mutation(async ({ ctx, input }) => {
|
|
203
|
-
// const workspace = await ctx.db.workspace.findFirst({
|
|
204
|
-
// where: { id: input.workspaceId, ownerId: ctx.session.user.id },
|
|
205
|
-
// });
|
|
206
|
-
// if (!workspace) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
207
|
-
// try {
|
|
208
|
-
// // Create content for AI processing
|
|
209
|
-
// const meetingContent = `
|
|
210
|
-
// Meeting Title: ${input.meetingData.title}
|
|
211
|
-
// Participants: ${input.meetingData.participants.join(', ')}
|
|
212
|
-
// Date: ${input.meetingData.date}
|
|
213
|
-
// Duration: ${input.meetingData.duration || 'Not specified'}
|
|
214
|
-
// Agenda: ${input.meetingData.agenda?.join(', ') || 'Not provided'}
|
|
215
|
-
// Notes: ${input.meetingData.notes || 'Not provided'}
|
|
216
|
-
// Transcript: ${input.meetingData.transcript || 'Not provided'}
|
|
217
|
-
// `;
|
|
218
|
-
// // Generate enhanced summary using GPT
|
|
219
|
-
// const summaryResponse = await openai.chat.completions.create({
|
|
220
|
-
// model: 'gpt-4-turbo-preview',
|
|
221
|
-
// messages: [
|
|
222
|
-
// {
|
|
223
|
-
// role: 'system',
|
|
224
|
-
// content: `You are a meeting summarizer. Given meeting information, create a comprehensive summary that includes:
|
|
225
|
-
// 1. Key discussion points
|
|
226
|
-
// 2. Action items
|
|
227
|
-
// 3. Decisions made
|
|
228
|
-
// 4. Next steps
|
|
229
|
-
// 5. Important insights
|
|
230
|
-
// Format the response as JSON with the following structure:
|
|
231
|
-
// {
|
|
232
|
-
// "keyPoints": ["point1", "point2"],
|
|
233
|
-
// "actionItems": ["action1", "action2"],
|
|
234
|
-
// "decisions": ["decision1", "decision2"],
|
|
235
|
-
// "nextSteps": ["step1", "step2"],
|
|
236
|
-
// "insights": ["insight1", "insight2"],
|
|
237
|
-
// "summary": "Overall meeting summary"
|
|
238
|
-
// }`
|
|
239
|
-
// },
|
|
240
|
-
// {
|
|
241
|
-
// role: 'user',
|
|
242
|
-
// content: `Please analyze and summarize this meeting information: ${meetingContent}`
|
|
243
|
-
// }
|
|
244
|
-
// ],
|
|
245
|
-
// });
|
|
246
|
-
// let summaryData;
|
|
247
|
-
// try {
|
|
248
|
-
// summaryData = JSON.parse(summaryResponse.choices[0]?.message?.content || '{}');
|
|
249
|
-
// } catch (parseError) {
|
|
250
|
-
// // Fallback if JSON parsing fails
|
|
251
|
-
// summaryData = {
|
|
252
|
-
// keyPoints: [],
|
|
253
|
-
// actionItems: [],
|
|
254
|
-
// decisions: [],
|
|
255
|
-
// nextSteps: [],
|
|
256
|
-
// insights: [],
|
|
257
|
-
// summary: summaryResponse.choices[0]?.message?.content || 'Unable to generate summary'
|
|
258
|
-
// };
|
|
259
|
-
// }
|
|
260
|
-
// // Create artifact in database
|
|
261
|
-
// const artifact = await ctx.db.artifact.create({
|
|
262
|
-
// data: {
|
|
263
|
-
// workspaceId: input.workspaceId,
|
|
264
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
265
|
-
// title: input.meetingData.title,
|
|
266
|
-
// content: JSON.stringify({
|
|
267
|
-
// originalData: input.meetingData,
|
|
268
|
-
// ...summaryData
|
|
269
|
-
// }),
|
|
270
|
-
// },
|
|
271
|
-
// });
|
|
272
|
-
// // Create initial version
|
|
273
|
-
// await ctx.db.artifactVersion.create({
|
|
274
|
-
// data: {
|
|
275
|
-
// artifactId: artifact.id,
|
|
276
|
-
// version: 1,
|
|
277
|
-
// content: artifact.content,
|
|
278
|
-
// },
|
|
279
|
-
// });
|
|
280
|
-
// return {
|
|
281
|
-
// id: artifact.id,
|
|
282
|
-
// title: artifact.title,
|
|
283
|
-
// summary: summaryData,
|
|
284
|
-
// originalData: input.meetingData,
|
|
285
|
-
// };
|
|
286
|
-
// } catch (error) {
|
|
287
|
-
// console.error('Error processing meeting schema:', error);
|
|
288
|
-
// throw new TRPCError({
|
|
289
|
-
// code: 'INTERNAL_SERVER_ERROR',
|
|
290
|
-
// message: 'Failed to process meeting data'
|
|
291
|
-
// });
|
|
292
|
-
// }
|
|
293
|
-
// }),
|
|
294
|
-
// // Update an existing meeting summary
|
|
295
|
-
// updateSummary: authedProcedure
|
|
296
|
-
// .input(z.object({
|
|
297
|
-
// summaryId: z.string(),
|
|
298
|
-
// title: z.string().optional(),
|
|
299
|
-
// content: z.string().optional(),
|
|
300
|
-
// }))
|
|
301
|
-
// .mutation(async ({ ctx, input }) => {
|
|
302
|
-
// const summary = await ctx.db.artifact.findFirst({
|
|
303
|
-
// where: {
|
|
304
|
-
// id: input.summaryId,
|
|
305
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
306
|
-
// workspace: { ownerId: ctx.session.user.id }
|
|
307
|
-
// },
|
|
308
|
-
// include: {
|
|
309
|
-
// versions: {
|
|
310
|
-
// orderBy: { version: 'desc' },
|
|
311
|
-
// take: 1,
|
|
312
|
-
// },
|
|
313
|
-
// },
|
|
314
|
-
// });
|
|
315
|
-
// if (!summary) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
316
|
-
// // Update artifact
|
|
317
|
-
// const updatedArtifact = await ctx.db.artifact.update({
|
|
318
|
-
// where: { id: input.summaryId },
|
|
319
|
-
// data: {
|
|
320
|
-
// title: input.title ?? summary.title,
|
|
321
|
-
// content: input.content ?? summary.content,
|
|
322
|
-
// updatedAt: new Date(),
|
|
323
|
-
// },
|
|
324
|
-
// });
|
|
325
|
-
// // Create new version if content changed
|
|
326
|
-
// if (input.content && input.content !== summary.content) {
|
|
327
|
-
// const latestVersion = summary.versions[0]?.version || 0;
|
|
328
|
-
// await ctx.db.artifactVersion.create({
|
|
329
|
-
// data: {
|
|
330
|
-
// artifactId: input.summaryId,
|
|
331
|
-
// version: latestVersion + 1,
|
|
332
|
-
// content: input.content,
|
|
333
|
-
// },
|
|
334
|
-
// });
|
|
335
|
-
// }
|
|
336
|
-
// return updatedArtifact;
|
|
337
|
-
// }),
|
|
338
|
-
// // Delete a meeting summary
|
|
339
|
-
// deleteSummary: authedProcedure
|
|
340
|
-
// .input(z.object({ summaryId: z.string() }))
|
|
341
|
-
// .mutation(async ({ ctx, input }) => {
|
|
342
|
-
// const summary = await ctx.db.artifact.findFirst({
|
|
343
|
-
// where: {
|
|
344
|
-
// id: input.summaryId,
|
|
345
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
346
|
-
// workspace: { ownerId: ctx.session.user.id }
|
|
347
|
-
// },
|
|
348
|
-
// });
|
|
349
|
-
// if (!summary) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
350
|
-
// // Delete associated versions first
|
|
351
|
-
// await ctx.db.artifactVersion.deleteMany({
|
|
352
|
-
// where: { artifactId: input.summaryId },
|
|
353
|
-
// });
|
|
354
|
-
// // Delete the artifact
|
|
355
|
-
// await ctx.db.artifact.delete({
|
|
356
|
-
// where: { id: input.summaryId },
|
|
357
|
-
// });
|
|
358
|
-
// return true;
|
|
359
|
-
// }),
|
|
360
|
-
// // Get meeting versions/history
|
|
361
|
-
// getVersions: authedProcedure
|
|
362
|
-
// .input(z.object({ summaryId: z.string() }))
|
|
363
|
-
// .query(async ({ ctx, input }) => {
|
|
364
|
-
// const summary = await ctx.db.artifact.findFirst({
|
|
365
|
-
// where: {
|
|
366
|
-
// id: input.summaryId,
|
|
367
|
-
// type: ArtifactType.MEETING_SUMMARY,
|
|
368
|
-
// workspace: { ownerId: ctx.session.user.id }
|
|
369
|
-
// },
|
|
370
|
-
// });
|
|
371
|
-
// if (!summary) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
372
|
-
// return ctx.db.artifactVersion.findMany({
|
|
373
|
-
// where: { artifactId: input.summaryId },
|
|
374
|
-
// orderBy: { version: 'desc' },
|
|
375
|
-
// });
|
|
376
|
-
// }),
|
|
377
|
-
// });
|