@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
package/dist/lib/auth.js CHANGED
@@ -1,44 +1,117 @@
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
+ */
2
11
  import crypto from "node:crypto";
3
- // Custom HMAC cookie: auth_token = base64(userId).hex(hmacSHA256(base64(userId), secret))
12
+ /**
13
+ * Verifies a custom HMAC-signed authentication cookie.
14
+ *
15
+ * The cookie format is: `base64(userId).hex(hmacSHA256(base64(userId), secret))`
16
+ *
17
+ * @param cookieValue - The raw cookie value to verify, or undefined if no cookie exists
18
+ * @returns Authentication result with userId if valid, null if invalid or missing
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const result = verifyCustomAuthCookie("dXNlcjEyMw.abc123def456...");
23
+ * if (result) {
24
+ * console.log(`Authenticated user: ${result.userId}`);
25
+ * }
26
+ * ```
27
+ *
28
+ * @throws {Error} Never throws - returns null for all error conditions
29
+ */
4
30
  export function verifyCustomAuthCookie(cookieValue) {
31
+ // Early return for missing cookie
5
32
  if (!cookieValue) {
6
33
  return null;
7
34
  }
35
+ // Get authentication secret from environment
8
36
  const secret = process.env.AUTH_SECRET;
9
37
  if (!secret) {
10
38
  return null;
11
39
  }
40
+ // Parse cookie format: base64UserId.signatureHex
12
41
  const parts = cookieValue.split(".");
13
42
  if (parts.length !== 2) {
14
43
  return null;
15
44
  }
16
45
  const [base64UserId, signatureHex] = parts;
17
- let userId;
46
+ // Decode the user ID from base64url encoding
47
+ const userId = decodeBase64UrlUserId(base64UserId);
48
+ if (!userId) {
49
+ return null;
50
+ }
51
+ // Verify the HMAC signature
52
+ const isValidSignature = verifyHmacSignature(base64UserId, signatureHex, secret);
53
+ if (!isValidSignature) {
54
+ return null;
55
+ }
56
+ return { userId };
57
+ }
58
+ /**
59
+ * Decodes a base64url-encoded user ID string.
60
+ *
61
+ * @param base64UserId - The base64url-encoded user ID
62
+ * @returns The decoded user ID string, or null if decoding fails
63
+ *
64
+ * @private
65
+ */
66
+ function decodeBase64UrlUserId(base64UserId) {
18
67
  try {
19
- const buf = Buffer.from(base64UserId, "base64url");
20
- userId = buf.toString("utf8");
68
+ const buffer = Buffer.from(base64UserId, "base64url");
69
+ return buffer.toString("utf8");
21
70
  }
22
71
  catch (error) {
23
72
  return null;
24
73
  }
74
+ }
75
+ /**
76
+ * Verifies an HMAC-SHA256 signature against the expected value.
77
+ *
78
+ * @param data - The data that was signed (base64url-encoded user ID)
79
+ * @param signatureHex - The hex-encoded signature to verify
80
+ * @param secret - The secret key used for signing
81
+ * @returns True if the signature is valid, false otherwise
82
+ *
83
+ * @private
84
+ */
85
+ function verifyHmacSignature(data, signatureHex, secret) {
25
86
  const hmac = crypto.createHmac("sha256", secret);
26
- hmac.update(base64UserId);
27
- const expected = hmac.digest("hex");
28
- if (!timingSafeEqualHex(signatureHex, expected)) {
29
- return null;
30
- }
31
- return { userId };
87
+ hmac.update(data);
88
+ const expectedSignature = hmac.digest("hex");
89
+ return timingSafeEqualHex(signatureHex, expectedSignature);
32
90
  }
91
+ /**
92
+ * Performs a timing-safe comparison of two hex-encoded strings.
93
+ *
94
+ * This function prevents timing attacks by ensuring the comparison
95
+ * takes the same amount of time regardless of where the strings differ.
96
+ *
97
+ * @param a - First hex string to compare
98
+ * @param b - Second hex string to compare
99
+ * @returns True if the strings are equal, false otherwise
100
+ *
101
+ * @private
102
+ */
33
103
  function timingSafeEqualHex(a, b) {
34
104
  try {
35
- const ab = Buffer.from(a, "hex");
36
- const bb = Buffer.from(b, "hex");
37
- if (ab.length !== bb.length)
105
+ const bufferA = Buffer.from(a, "hex");
106
+ const bufferB = Buffer.from(b, "hex");
107
+ // Length check prevents timing attacks on different-length inputs
108
+ if (bufferA.length !== bufferB.length) {
38
109
  return false;
39
- return crypto.timingSafeEqual(ab, bb);
110
+ }
111
+ return crypto.timingSafeEqual(bufferA, bufferB);
40
112
  }
41
113
  catch {
114
+ // Return false for any parsing errors
42
115
  return false;
43
116
  }
44
117
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Parsed and validated environment variables
3
+ */
4
+ export declare const env: {
5
+ DATABASE_URL: string;
6
+ PORT: number;
7
+ NODE_ENV: "production" | "development" | "test";
8
+ FRONTEND_URL: string;
9
+ DIRECT_URL?: string | undefined;
10
+ BETTER_AUTH_SECRET?: string | undefined;
11
+ BETTER_AUTH_URL?: string | undefined;
12
+ GOOGLE_CLOUD_PROJECT_ID?: string | undefined;
13
+ GOOGLE_CLOUD_BUCKET_NAME?: string | undefined;
14
+ GOOGLE_APPLICATION_CREDENTIALS?: string | undefined;
15
+ PUSHER_APP_ID?: string | undefined;
16
+ PUSHER_KEY?: string | undefined;
17
+ PUSHER_SECRET?: string | undefined;
18
+ PUSHER_CLUSTER?: string | undefined;
19
+ INFERENCE_API_URL?: string | undefined;
20
+ };
21
+ /**
22
+ * Check if running in production
23
+ */
24
+ export declare const isProduction: boolean;
25
+ /**
26
+ * Check if running in development
27
+ */
28
+ export declare const isDevelopment: boolean;
29
+ /**
30
+ * Check if running in test
31
+ */
32
+ export declare const isTest: boolean;
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import dotenv from 'dotenv';
3
+ dotenv.config();
4
+ /**
5
+ * Environment variable schema
6
+ */
7
+ const envSchema = z.object({
8
+ // Database
9
+ DATABASE_URL: z.string().url(),
10
+ DIRECT_URL: z.string().url().optional(),
11
+ // Server
12
+ PORT: z.string().regex(/^\d+$/).default('3001').transform(Number),
13
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
14
+ // Auth
15
+ BETTER_AUTH_SECRET: z.string().min(32).optional(),
16
+ BETTER_AUTH_URL: z.string().url().optional(),
17
+ // Storage
18
+ GOOGLE_CLOUD_PROJECT_ID: z.string().optional(),
19
+ GOOGLE_CLOUD_BUCKET_NAME: z.string().optional(),
20
+ GOOGLE_APPLICATION_CREDENTIALS: z.string().optional(),
21
+ // Pusher
22
+ PUSHER_APP_ID: z.string().optional(),
23
+ PUSHER_KEY: z.string().optional(),
24
+ PUSHER_SECRET: z.string().optional(),
25
+ PUSHER_CLUSTER: z.string().optional(),
26
+ // Inference
27
+ INFERENCE_API_URL: z.string().url().optional(),
28
+ // CORS
29
+ FRONTEND_URL: z.string().url().default('http://localhost:3000'),
30
+ });
31
+ /**
32
+ * Parsed and validated environment variables
33
+ */
34
+ export const env = envSchema.parse(process.env);
35
+ /**
36
+ * Check if running in production
37
+ */
38
+ export const isProduction = env.NODE_ENV === 'production';
39
+ /**
40
+ * Check if running in development
41
+ */
42
+ export const isDevelopment = env.NODE_ENV === 'development';
43
+ /**
44
+ * Check if running in test
45
+ */
46
+ export const isTest = env.NODE_ENV === 'test';
@@ -0,0 +1,33 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ /**
3
+ * Custom error classes for better error handling
4
+ */
5
+ export declare class AppError extends Error {
6
+ code: string;
7
+ statusCode: number;
8
+ isOperational: boolean;
9
+ constructor(message: string, code: string, statusCode?: number, isOperational?: boolean);
10
+ }
11
+ export declare class ValidationError extends AppError {
12
+ constructor(message: string);
13
+ }
14
+ export declare class NotFoundError extends AppError {
15
+ constructor(resource?: string);
16
+ }
17
+ export declare class UnauthorizedError extends AppError {
18
+ constructor(message?: string);
19
+ }
20
+ export declare class ForbiddenError extends AppError {
21
+ constructor(message?: string);
22
+ }
23
+ export declare class ConflictError extends AppError {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Convert AppError to TRPCError
28
+ */
29
+ export declare function toTRPCError(error: unknown): TRPCError;
30
+ /**
31
+ * Error handler for async functions
32
+ */
33
+ export declare function asyncHandler<T extends (...args: any[]) => Promise<any>>(fn: T): T;
@@ -0,0 +1,78 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ /**
3
+ * Custom error classes for better error handling
4
+ */
5
+ export class AppError extends Error {
6
+ constructor(message, code, statusCode = 500, isOperational = true) {
7
+ super(message);
8
+ this.code = code;
9
+ this.statusCode = statusCode;
10
+ this.isOperational = isOperational;
11
+ this.name = this.constructor.name;
12
+ Error.captureStackTrace(this, this.constructor);
13
+ }
14
+ }
15
+ export class ValidationError extends AppError {
16
+ constructor(message) {
17
+ super(message, 'VALIDATION_ERROR', 400);
18
+ }
19
+ }
20
+ export class NotFoundError extends AppError {
21
+ constructor(resource = 'Resource') {
22
+ super(`${resource} not found`, 'NOT_FOUND', 404);
23
+ }
24
+ }
25
+ export class UnauthorizedError extends AppError {
26
+ constructor(message = 'Unauthorized') {
27
+ super(message, 'UNAUTHORIZED', 401);
28
+ }
29
+ }
30
+ export class ForbiddenError extends AppError {
31
+ constructor(message = 'Forbidden') {
32
+ super(message, 'FORBIDDEN', 403);
33
+ }
34
+ }
35
+ export class ConflictError extends AppError {
36
+ constructor(message) {
37
+ super(message, 'CONFLICT', 409);
38
+ }
39
+ }
40
+ /**
41
+ * Convert AppError to TRPCError
42
+ */
43
+ export function toTRPCError(error) {
44
+ if (error instanceof AppError) {
45
+ const codeMap = {
46
+ 400: 'BAD_REQUEST',
47
+ 401: 'UNAUTHORIZED',
48
+ 403: 'FORBIDDEN',
49
+ 404: 'NOT_FOUND',
50
+ 409: 'CONFLICT',
51
+ 500: 'INTERNAL_SERVER_ERROR',
52
+ };
53
+ return new TRPCError({
54
+ code: codeMap[error.statusCode] || 'INTERNAL_SERVER_ERROR',
55
+ message: error.message,
56
+ cause: error,
57
+ });
58
+ }
59
+ if (error instanceof TRPCError) {
60
+ return error;
61
+ }
62
+ // Default error
63
+ return new TRPCError({
64
+ code: 'INTERNAL_SERVER_ERROR',
65
+ message: error instanceof Error ? error.message : 'An unexpected error occurred',
66
+ cause: error,
67
+ });
68
+ }
69
+ /**
70
+ * Error handler for async functions
71
+ */
72
+ export function asyncHandler(fn) {
73
+ return ((...args) => {
74
+ return Promise.resolve(fn(...args)).catch((error) => {
75
+ throw toTRPCError(error);
76
+ });
77
+ });
78
+ }
@@ -1,2 +1,5 @@
1
- declare function inference(prompt: string, tag: string): Promise<Response>;
1
+ import OpenAI from 'openai';
2
+ declare function inference(prompt: string): Promise<OpenAI.Chat.Completions.ChatCompletion & {
3
+ _request_id?: string | null;
4
+ }>;
2
5
  export default inference;
@@ -1,15 +1,13 @@
1
- async function inference(prompt, tag) {
1
+ import OpenAI from 'openai';
2
+ const openai = new OpenAI({
3
+ apiKey: process.env.INFERENCE_API_KEY,
4
+ baseURL: process.env.INFERENCE_BASE_URL,
5
+ });
6
+ async function inference(prompt) {
2
7
  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
- }),
8
+ const response = await openai.chat.completions.create({
9
+ model: "command-a-03-2025",
10
+ messages: [{ role: "user", content: prompt }],
13
11
  });
14
12
  return response;
15
13
  }
@@ -0,0 +1,62 @@
1
+ export declare enum LogLevel {
2
+ ERROR = 0,
3
+ WARN = 1,
4
+ INFO = 2,
5
+ DEBUG = 3,
6
+ TRACE = 4
7
+ }
8
+ export interface LogEntry {
9
+ timestamp: string;
10
+ level: string;
11
+ message: string;
12
+ context?: string;
13
+ metadata?: Record<string, any>;
14
+ error?: {
15
+ name: string;
16
+ message: string;
17
+ stack?: string;
18
+ };
19
+ }
20
+ export interface LoggerConfig {
21
+ level: LogLevel;
22
+ enableConsole: boolean;
23
+ enableFile: boolean;
24
+ logDir?: string;
25
+ maxFileSize?: number;
26
+ maxFiles?: number;
27
+ format?: 'json' | 'pretty';
28
+ }
29
+ declare class Logger {
30
+ private config;
31
+ private logStream?;
32
+ constructor(config?: Partial<LoggerConfig>);
33
+ private setupFileLogging;
34
+ private shouldLog;
35
+ private formatLogEntry;
36
+ private formatLevel;
37
+ private formatTimestamp;
38
+ private formatContext;
39
+ private formatMetadata;
40
+ private formatValue;
41
+ private formatError;
42
+ private log;
43
+ error(message: string, context?: string, metadata?: Record<string, any>, error?: Error): void;
44
+ warn(message: string, context?: string, metadata?: Record<string, any>): void;
45
+ info(message: string, context?: string, metadata?: Record<string, any>): void;
46
+ debug(message: string, context?: string, metadata?: Record<string, any>): void;
47
+ trace(message: string, context?: string, metadata?: Record<string, any>): void;
48
+ http(method: string, url: string, statusCode: number, responseTime?: number, context?: string): void;
49
+ private getHttpStatusIcon;
50
+ database(operation: string, table: string, duration?: number, context?: string): void;
51
+ private getDatabaseOperationIcon;
52
+ auth(action: string, userId?: string, context?: string): void;
53
+ trpc(procedure: string, input?: any, output?: any, duration?: number, context?: string): void;
54
+ updateConfig(newConfig: Partial<LoggerConfig>): void;
55
+ progress(message: string, current: number, total: number, context?: string): void;
56
+ private createProgressBar;
57
+ success(message: string, context?: string, metadata?: Record<string, any>): void;
58
+ failure(message: string, context?: string, metadata?: Record<string, any>, error?: Error): void;
59
+ close(): void;
60
+ }
61
+ export declare const logger: Logger;
62
+ export { Logger };