@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.
- package/ANALYSIS_PROGRESS_SPEC.md +463 -0
- package/PROGRESS_QUICK_REFERENCE.md +239 -0
- package/dist/lib/ai-session.d.ts +20 -9
- package/dist/lib/ai-session.js +316 -80
- package/dist/lib/auth.d.ts +35 -2
- package/dist/lib/auth.js +88 -15
- package/dist/lib/env.d.ts +32 -0
- package/dist/lib/env.js +46 -0
- package/dist/lib/errors.d.ts +33 -0
- package/dist/lib/errors.js +78 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +9 -11
- package/dist/lib/logger.d.ts +62 -0
- package/dist/lib/logger.js +342 -0
- package/dist/lib/podcast-prompts.d.ts +43 -0
- package/dist/lib/podcast-prompts.js +135 -0
- package/dist/lib/pusher.d.ts +1 -0
- package/dist/lib/pusher.js +14 -2
- package/dist/lib/storage.d.ts +3 -3
- package/dist/lib/storage.js +51 -47
- package/dist/lib/validation.d.ts +51 -0
- package/dist/lib/validation.js +64 -0
- package/dist/routers/_app.d.ts +697 -111
- package/dist/routers/_app.js +5 -0
- package/dist/routers/auth.d.ts +11 -1
- package/dist/routers/chat.d.ts +11 -1
- package/dist/routers/flashcards.d.ts +205 -6
- package/dist/routers/flashcards.js +144 -66
- package/dist/routers/members.d.ts +165 -0
- package/dist/routers/members.js +531 -0
- package/dist/routers/podcast.d.ts +78 -63
- package/dist/routers/podcast.js +330 -393
- package/dist/routers/studyguide.d.ts +11 -1
- package/dist/routers/worksheets.d.ts +124 -13
- package/dist/routers/worksheets.js +123 -50
- package/dist/routers/workspace.d.ts +213 -26
- package/dist/routers/workspace.js +303 -181
- package/dist/server.js +12 -4
- package/dist/services/flashcard-progress.service.d.ts +183 -0
- package/dist/services/flashcard-progress.service.js +383 -0
- package/dist/services/flashcard.service.d.ts +183 -0
- package/dist/services/flashcard.service.js +224 -0
- package/dist/services/podcast-segment-reorder.d.ts +0 -0
- package/dist/services/podcast-segment-reorder.js +107 -0
- package/dist/services/podcast.service.d.ts +0 -0
- package/dist/services/podcast.service.js +326 -0
- package/dist/services/worksheet.service.d.ts +0 -0
- package/dist/services/worksheet.service.js +295 -0
- package/dist/trpc.d.ts +13 -2
- package/dist/trpc.js +55 -6
- package/dist/types/index.d.ts +126 -0
- package/dist/types/index.js +1 -0
- package/package.json +3 -2
- package/prisma/schema.prisma +142 -4
- package/src/lib/ai-session.ts +356 -85
- package/src/lib/auth.ts +113 -19
- package/src/lib/env.ts +59 -0
- package/src/lib/errors.ts +92 -0
- package/src/lib/inference.ts +11 -11
- package/src/lib/logger.ts +405 -0
- package/src/lib/pusher.ts +15 -3
- package/src/lib/storage.ts +56 -51
- package/src/lib/validation.ts +75 -0
- package/src/routers/_app.ts +5 -0
- package/src/routers/chat.ts +2 -23
- package/src/routers/flashcards.ts +108 -24
- package/src/routers/members.ts +586 -0
- package/src/routers/podcast.ts +385 -420
- package/src/routers/worksheets.ts +117 -35
- package/src/routers/workspace.ts +328 -195
- package/src/server.ts +13 -4
- package/src/services/flashcard-progress.service.ts +541 -0
- package/src/trpc.ts +59 -6
- package/src/types/index.ts +165 -0
- package/AUTH_FRONTEND_SPEC.md +0 -21
- package/CHAT_FRONTEND_SPEC.md +0 -474
- package/DATABASE_SETUP.md +0 -165
- package/MEETINGSUMMARY_FRONTEND_SPEC.md +0 -28
- package/PODCAST_FRONTEND_SPEC.md +0 -595
- package/STUDYGUIDE_FRONTEND_SPEC.md +0 -18
- package/WORKSHEETS_FRONTEND_SPEC.md +0 -26
- package/WORKSPACE_FRONTEND_SPEC.md +0 -47
- package/test-ai-integration.js +0 -134
package/src/lib/auth.ts
CHANGED
|
@@ -1,51 +1,145 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
|
package/src/lib/inference.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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) {
|