@goscribe/server 1.0.11 → 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
package/src/lib/auth.ts CHANGED
@@ -1,51 +1,145 @@
1
- // src/server/auth.ts
1
+ /**
2
+ * Authentication utilities for custom HMAC-based cookie verification.
3
+ *
4
+ * This module provides secure authentication using HMAC-SHA256 signatures
5
+ * to verify user identity through signed cookies.
6
+ *
7
+ * @fileoverview Custom authentication system with HMAC cookie verification
8
+ * @author Scribe Team
9
+ * @version 1.0.0
10
+ */
11
+
2
12
  import crypto from "node:crypto";
3
13
 
4
- // Custom HMAC cookie: auth_token = base64(userId).hex(hmacSHA256(base64(userId), secret))
5
- export function verifyCustomAuthCookie(cookieValue: string | undefined): { userId: string } | null {
14
+ /**
15
+ * Represents the result of successful authentication verification.
16
+ */
17
+ export interface AuthResult {
18
+ /** The authenticated user's unique identifier */
19
+ userId: string;
20
+ }
21
+
22
+ /**
23
+ * Configuration for the authentication system.
24
+ */
25
+ interface AuthConfig {
26
+ /** The secret key used for HMAC signing */
27
+ secret: string;
28
+ }
29
+
30
+ /**
31
+ * Verifies a custom HMAC-signed authentication cookie.
32
+ *
33
+ * The cookie format is: `base64(userId).hex(hmacSHA256(base64(userId), secret))`
34
+ *
35
+ * @param cookieValue - The raw cookie value to verify, or undefined if no cookie exists
36
+ * @returns Authentication result with userId if valid, null if invalid or missing
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const result = verifyCustomAuthCookie("dXNlcjEyMw.abc123def456...");
41
+ * if (result) {
42
+ * console.log(`Authenticated user: ${result.userId}`);
43
+ * }
44
+ * ```
45
+ *
46
+ * @throws {Error} Never throws - returns null for all error conditions
47
+ */
48
+ export function verifyCustomAuthCookie(cookieValue: string | undefined): AuthResult | null {
49
+ // Early return for missing cookie
6
50
  if (!cookieValue) {
7
51
  return null;
8
52
  }
9
53
 
54
+ // Get authentication secret from environment
10
55
  const secret = process.env.AUTH_SECRET;
11
-
12
56
  if (!secret) {
13
57
  return null;
14
58
  }
15
59
 
60
+ // Parse cookie format: base64UserId.signatureHex
16
61
  const parts = cookieValue.split(".");
17
-
18
62
  if (parts.length !== 2) {
19
63
  return null;
20
64
  }
65
+
21
66
  const [base64UserId, signatureHex] = parts;
22
67
 
23
- let userId: string;
24
- try {
25
- const buf = Buffer.from(base64UserId, "base64url");
26
- userId = buf.toString("utf8");
27
- } catch (error) {
68
+ // Decode the user ID from base64url encoding
69
+ const userId = decodeBase64UrlUserId(base64UserId);
70
+ if (!userId) {
28
71
  return null;
29
72
  }
30
73
 
31
- const hmac = crypto.createHmac("sha256", secret);
32
- hmac.update(base64UserId);
33
- const expected = hmac.digest("hex");
34
-
35
- if (!timingSafeEqualHex(signatureHex, expected)) {
74
+ // Verify the HMAC signature
75
+ const isValidSignature = verifyHmacSignature(base64UserId, signatureHex, secret);
76
+ if (!isValidSignature) {
36
77
  return null;
37
78
  }
38
79
 
39
80
  return { userId };
40
81
  }
41
82
 
83
+ /**
84
+ * Decodes a base64url-encoded user ID string.
85
+ *
86
+ * @param base64UserId - The base64url-encoded user ID
87
+ * @returns The decoded user ID string, or null if decoding fails
88
+ *
89
+ * @private
90
+ */
91
+ function decodeBase64UrlUserId(base64UserId: string): string | null {
92
+ try {
93
+ const buffer = Buffer.from(base64UserId, "base64url");
94
+ return buffer.toString("utf8");
95
+ } catch (error) {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Verifies an HMAC-SHA256 signature against the expected value.
102
+ *
103
+ * @param data - The data that was signed (base64url-encoded user ID)
104
+ * @param signatureHex - The hex-encoded signature to verify
105
+ * @param secret - The secret key used for signing
106
+ * @returns True if the signature is valid, false otherwise
107
+ *
108
+ * @private
109
+ */
110
+ function verifyHmacSignature(data: string, signatureHex: string, secret: string): boolean {
111
+ const hmac = crypto.createHmac("sha256", secret);
112
+ hmac.update(data);
113
+ const expectedSignature = hmac.digest("hex");
114
+
115
+ return timingSafeEqualHex(signatureHex, expectedSignature);
116
+ }
117
+
118
+ /**
119
+ * Performs a timing-safe comparison of two hex-encoded strings.
120
+ *
121
+ * This function prevents timing attacks by ensuring the comparison
122
+ * takes the same amount of time regardless of where the strings differ.
123
+ *
124
+ * @param a - First hex string to compare
125
+ * @param b - Second hex string to compare
126
+ * @returns True if the strings are equal, false otherwise
127
+ *
128
+ * @private
129
+ */
42
130
  function timingSafeEqualHex(a: string, b: string): boolean {
43
131
  try {
44
- const ab = Buffer.from(a, "hex");
45
- const bb = Buffer.from(b, "hex");
46
- if (ab.length !== bb.length) return false;
47
- return crypto.timingSafeEqual(ab, bb);
132
+ const bufferA = Buffer.from(a, "hex");
133
+ const bufferB = Buffer.from(b, "hex");
134
+
135
+ // Length check prevents timing attacks on different-length inputs
136
+ if (bufferA.length !== bufferB.length) {
137
+ return false;
138
+ }
139
+
140
+ return crypto.timingSafeEqual(bufferA, bufferB);
48
141
  } catch {
142
+ // Return false for any parsing errors
49
143
  return false;
50
144
  }
51
145
  }
package/src/lib/env.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+
4
+ dotenv.config();
5
+
6
+ /**
7
+ * Environment variable schema
8
+ */
9
+ const envSchema = z.object({
10
+ // Database
11
+ DATABASE_URL: z.string().url(),
12
+ DIRECT_URL: z.string().url().optional(),
13
+
14
+ // Server
15
+ PORT: z.string().regex(/^\d+$/).default('3001').transform(Number),
16
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
17
+
18
+ // Auth
19
+ BETTER_AUTH_SECRET: z.string().min(32).optional(),
20
+ BETTER_AUTH_URL: z.string().url().optional(),
21
+
22
+ // Storage
23
+ GOOGLE_CLOUD_PROJECT_ID: z.string().optional(),
24
+ GOOGLE_CLOUD_BUCKET_NAME: z.string().optional(),
25
+ GOOGLE_APPLICATION_CREDENTIALS: z.string().optional(),
26
+
27
+ // Pusher
28
+ PUSHER_APP_ID: z.string().optional(),
29
+ PUSHER_KEY: z.string().optional(),
30
+ PUSHER_SECRET: z.string().optional(),
31
+ PUSHER_CLUSTER: z.string().optional(),
32
+
33
+ // Inference
34
+ INFERENCE_API_URL: z.string().url().optional(),
35
+
36
+ // CORS
37
+ FRONTEND_URL: z.string().url().default('http://localhost:3000'),
38
+ });
39
+
40
+ /**
41
+ * Parsed and validated environment variables
42
+ */
43
+ export const env = envSchema.parse(process.env);
44
+
45
+ /**
46
+ * Check if running in production
47
+ */
48
+ export const isProduction = env.NODE_ENV === 'production';
49
+
50
+ /**
51
+ * Check if running in development
52
+ */
53
+ export const isDevelopment = env.NODE_ENV === 'development';
54
+
55
+ /**
56
+ * Check if running in test
57
+ */
58
+ export const isTest = env.NODE_ENV === 'test';
59
+
@@ -0,0 +1,92 @@
1
+ import { TRPCError } from '@trpc/server';
2
+
3
+ /**
4
+ * Custom error classes for better error handling
5
+ */
6
+ export class AppError extends Error {
7
+ constructor(
8
+ message: string,
9
+ public code: string,
10
+ public statusCode: number = 500,
11
+ public isOperational: boolean = true
12
+ ) {
13
+ super(message);
14
+ this.name = this.constructor.name;
15
+ Error.captureStackTrace(this, this.constructor);
16
+ }
17
+ }
18
+
19
+ export class ValidationError extends AppError {
20
+ constructor(message: string) {
21
+ super(message, 'VALIDATION_ERROR', 400);
22
+ }
23
+ }
24
+
25
+ export class NotFoundError extends AppError {
26
+ constructor(resource: string = 'Resource') {
27
+ super(`${resource} not found`, 'NOT_FOUND', 404);
28
+ }
29
+ }
30
+
31
+ export class UnauthorizedError extends AppError {
32
+ constructor(message: string = 'Unauthorized') {
33
+ super(message, 'UNAUTHORIZED', 401);
34
+ }
35
+ }
36
+
37
+ export class ForbiddenError extends AppError {
38
+ constructor(message: string = 'Forbidden') {
39
+ super(message, 'FORBIDDEN', 403);
40
+ }
41
+ }
42
+
43
+ export class ConflictError extends AppError {
44
+ constructor(message: string) {
45
+ super(message, 'CONFLICT', 409);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Convert AppError to TRPCError
51
+ */
52
+ export function toTRPCError(error: unknown): TRPCError {
53
+ if (error instanceof AppError) {
54
+ const codeMap: Record<number, any> = {
55
+ 400: 'BAD_REQUEST',
56
+ 401: 'UNAUTHORIZED',
57
+ 403: 'FORBIDDEN',
58
+ 404: 'NOT_FOUND',
59
+ 409: 'CONFLICT',
60
+ 500: 'INTERNAL_SERVER_ERROR',
61
+ };
62
+
63
+ return new TRPCError({
64
+ code: codeMap[error.statusCode] || 'INTERNAL_SERVER_ERROR',
65
+ message: error.message,
66
+ cause: error,
67
+ });
68
+ }
69
+
70
+ if (error instanceof TRPCError) {
71
+ return error;
72
+ }
73
+
74
+ // Default error
75
+ return new TRPCError({
76
+ code: 'INTERNAL_SERVER_ERROR',
77
+ message: error instanceof Error ? error.message : 'An unexpected error occurred',
78
+ cause: error,
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Error handler for async functions
84
+ */
85
+ export function asyncHandler<T extends (...args: any[]) => Promise<any>>(fn: T): T {
86
+ return ((...args: Parameters<T>) => {
87
+ return Promise.resolve(fn(...args)).catch((error) => {
88
+ throw toTRPCError(error);
89
+ });
90
+ }) as T;
91
+ }
92
+
@@ -1,15 +1,15 @@
1
- async function inference(prompt: string, tag: string) {
1
+ import OpenAI from 'openai';
2
+
3
+ const openai = new OpenAI({
4
+ apiKey: process.env.INFERENCE_API_KEY,
5
+ baseURL: process.env.INFERENCE_BASE_URL,
6
+ });
7
+
8
+ async function inference(prompt: string) {
2
9
  try {
3
- const response = await fetch("https://proxy-ai.onrender.com/api/cohere/inference", {
4
- method: "POST",
5
- headers: {
6
- "Content-Type": "application/json",
7
- },
8
- body: JSON.stringify({
9
- prompt: prompt,
10
- model: "command-r-plus",
11
- max_tokens: 2000,
12
- }),
10
+ const response = await openai.chat.completions.create({
11
+ model: "command-a-03-2025",
12
+ messages: [{ role: "user", content: prompt }],
13
13
  });
14
14
  return response;
15
15
  } catch (error) {