CognxSafeTrack commited on
Commit
aec4d7f
·
1 Parent(s): ab4089a

feat: implement Phase 4 (Quota management, centralized logging, and env sync)

Browse files
.env.example CHANGED
@@ -1,46 +1,35 @@
1
- # Environment Variables — EdTech
2
-
3
- # Copy this file to .env and fill in the values.
4
- # NEVER commit the real .env file to git.
5
-
6
- # ─── API Auth ──────────────────────────────────────────────────────────────────
7
- ADMIN_API_KEY= # Strong random secret, e.g. openssl rand -hex 32
8
-
9
- # ─── WhatsApp / Meta ───────────────────────────────────────────────────────────
10
- WHATSAPP_VERIFY_TOKEN= # Token you set in the Meta App dashboard
11
- WHATSAPP_APP_SECRET= # App Secret from Meta App Settings > Basic
12
- WHATSAPP_ACCESS_TOKEN= # Permanent System User token from Meta Business
13
- WHATSAPP_PHONE_NUMBER_ID= # Phone Number ID from Meta App > WhatsApp > Getting Started
14
-
15
- # ─── Database ──────────────────────────────────────────────────────────────────
16
- DATABASE_URL=postgresql://user:password@localhost:5432/edtech?schema=public
17
-
18
- # ─── Redis ─────────────────────────────────────────────────────────────────────
19
- REDIS_URL= # e.g. redis://default:password@host:6379
20
- # Or individual connection params (if REDIS_URL is not set):
21
- # REDIS_HOST=localhost
22
- # REDIS_PORT=6379
23
- # REDIS_USERNAME=default
24
- # REDIS_PASSWORD=
25
- # REDIS_TLS=false
26
-
27
- # ─── Stripe ────────────────────────────────────────────────────────────────────
28
- STRIPE_SECRET_KEY= # sk_live_... (or sk_test_... for dev)
29
- STRIPE_WEBHOOK_SECRET= # whsec_... from Stripe dashboard > Webhooks
30
-
31
- # ─── OpenAI / AI ───────────────────────────────────────────────────────────────
32
- OPENAI_API_KEY= # sk-...
33
-
34
- # ─── Cloudflare R2 Storage ────────────────────────────────────────────────────
35
- R2_ACCOUNT_ID= # Cloudflare Account ID
36
- R2_ACCESS_KEY_ID= # R2 API token Access Key ID
37
- R2_SECRET_ACCESS_KEY= # R2 API token Secret Access Key
38
- R2_BUCKET= # R2 bucket name (e.g. edtech-docs)
39
- R2_PUBLIC_URL= # Public URL of the bucket (e.g. https://pub-xxx.r2.dev)
40
-
41
- # ─── Frontend ──────────────────────────────────────────────────────────────────
42
- VITE_CLIENT_URL=https://your-frontend.netlify.app
43
- VITE_WHATSAPP_NUMBER=221XXXXXXXXX # Replace with your production WhatsApp Bot number
44
-
45
- # ─── Internal (Worker → API) ───────────────────────────────────────────────────
46
- API_URL=http://localhost:3001 # In prod: full URL of the Fastify API
 
1
+ # ─── DATABASE ─────────────────────────────────────────────────────────────────
2
+ DATABASE_URL="postgresql://user:pass@host:port/db?schema=public"
3
+ REDIS_URL="redis://localhost:6379"
4
+
5
+ # ─── SECURITY ─────────────────────────────────────────────────────────────────
6
+ JWT_SECRET="your-super-secret-jwt-key"
7
+ ADMIN_API_KEY="your-internal-admin-key"
8
+ ENCRYPTION_SECRET="your-32-char-encryption-key-for-secrets"
9
+ WHATSAPP_VERIFY_TOKEN="your-meta-webhook-verify-token"
10
+ WHATSAPP_APP_SECRET="your-meta-app-secret-for-hmac"
11
+
12
+ # ─── PROVIDERS (GLOBAL FALLBACKS) ─────────────────────────────────────────────
13
+ OPENAI_API_KEY="sk-..."
14
+ GOOGLE_AI_API_KEY="AIza..."
15
+ STRIPE_SECRET_KEY="sk_test_..."
16
+ STRIPE_WEBHOOK_SECRET="whsec_..."
17
+ STRIPE_PAAS_SUBSCRIPTION_PRICE_ID="price_..."
18
+
19
+ # ─── INFRASTRUCTURE ───────────────────────────────────────────────────────────
20
+ PORT=8080
21
+ NODE_ENV="production"
22
+ VITE_CLIENT_URL="https://admin.xamle.studio"
23
+ RAILWAY_INTERNAL_URL="http://whatsapp-worker.railway.internal:8082"
24
+ RAILWAY_PUBLIC_URL="https://api.xamle.studio"
25
+
26
+ # ─── STORAGE (R2/S3) ───────────���──────────────────────────────────────────────
27
+ R2_ACCESS_KEY_ID="..."
28
+ R2_SECRET_ACCESS_KEY="..."
29
+ R2_ENDPOINT="https://<accountid>.r2.cloudflarestorage.com"
30
+ R2_BUCKET_NAME="edtech-media"
31
+ R2_PUBLIC_URL="https://pub-....r2.dev"
32
+
33
+ # ─── OBSERVABILITY ────────────────────────────────────────────────────────────
34
+ LOG_LEVEL="info"
35
+ LOGTAIL_TOKEN="your-betterstack-token" # Optional: for centralized logging
 
 
 
 
 
 
 
 
 
 
 
apps/api/package.json CHANGED
@@ -20,6 +20,8 @@
20
  "@fastify/rate-limit": "^9.1.0",
21
  "@fastify/static": "^9.1.3",
22
  "@google/generative-ai": "^0.24.1",
 
 
23
  "@prisma/client": "^5.0.0",
24
  "@repo/database": "workspace:*",
25
  "@repo/prompts": "workspace:*",
@@ -36,6 +38,7 @@
36
  "ioredis": "^5.9.3",
37
  "node-cron": "^4.2.1",
38
  "openai": "^4.0.0",
 
39
  "pptxgenjs": "^3.12.0",
40
  "puppeteer": "^22.0.0",
41
  "stripe": "^20.3.1",
 
20
  "@fastify/rate-limit": "^9.1.0",
21
  "@fastify/static": "^9.1.3",
22
  "@google/generative-ai": "^0.24.1",
23
+ "@logtail/node": "^0.5.8",
24
+ "@logtail/pino": "^0.5.8",
25
  "@prisma/client": "^5.0.0",
26
  "@repo/database": "workspace:*",
27
  "@repo/prompts": "workspace:*",
 
38
  "ioredis": "^5.9.3",
39
  "node-cron": "^4.2.1",
40
  "openai": "^4.0.0",
41
+ "pino-pretty": "^13.1.3",
42
  "pptxgenjs": "^3.12.0",
43
  "puppeteer": "^22.0.0",
44
  "stripe": "^20.3.1",
apps/api/src/logger.ts CHANGED
@@ -1,59 +1,35 @@
1
  import pino from 'pino';
2
- import { getOrganizationId } from '@repo/database';
3
 
4
- const pinoLogger = pino({
5
- level: process.env.LOG_LEVEL || 'info',
6
- transport: process.env.NODE_ENV !== 'production' ? {
7
- target: 'pino-pretty',
8
- options: {
9
- colorize: true,
10
- translateTime: 'SYS:standard',
11
- ignore: 'pid,hostname'
12
- }
13
- } : undefined
 
 
14
  });
15
 
16
- function getEnrichedObject(obj: any = {}) {
17
- const organizationId = getOrganizationId();
18
- if (organizationId) {
19
- return { ...obj, organizationId };
20
- }
21
- return obj;
 
22
  }
23
 
24
- export const logger = {
25
- info: (first: any, ...rest: any[]) => {
26
- const orgId = getOrganizationId();
27
- if (typeof first === 'string') {
28
- pinoLogger.info(orgId ? { organizationId: orgId } : {}, first, ...rest);
29
- } else {
30
- pinoLogger.info(getEnrichedObject(first), rest[0] || '', ...rest.slice(1));
31
- }
32
- },
33
- error: (first: any, ...rest: any[]) => {
34
- const orgId = getOrganizationId();
35
- if (first instanceof Error) {
36
- pinoLogger.error(getEnrichedObject({ err: first }), rest[0] || first.message, ...rest.slice(1));
37
- } else if (typeof first === 'string') {
38
- pinoLogger.error(orgId ? { organizationId: orgId } : {}, first, ...rest);
39
- } else {
40
- pinoLogger.error(getEnrichedObject(first), rest[0] || '', ...rest.slice(1));
41
- }
42
- },
43
- warn: (first: any, ...rest: any[]) => {
44
- const orgId = getOrganizationId();
45
- if (typeof first === 'string') {
46
- pinoLogger.warn(orgId ? { organizationId: orgId } : {}, first, ...rest);
47
- } else {
48
- pinoLogger.warn(getEnrichedObject(first), rest[0] || '', ...rest.slice(1));
49
- }
50
- },
51
- debug: (first: any, ...rest: any[]) => {
52
- const orgId = getOrganizationId();
53
- if (typeof first === 'string') {
54
- pinoLogger.debug(orgId ? { organizationId: orgId } : {}, first, ...rest);
55
- } else {
56
- pinoLogger.debug(getEnrichedObject(first), rest[0] || '', ...rest.slice(1));
57
- }
58
- },
59
- };
 
1
  import pino from 'pino';
 
2
 
3
+ // Prepare transports
4
+ const transports = [];
5
+
6
+ // 1. Console transport (Standard)
7
+ transports.push({
8
+ target: 'pino-pretty',
9
+ options: {
10
+ colorize: true,
11
+ ignore: 'pid,hostname',
12
+ translateTime: 'SYS:standard',
13
+ },
14
+ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
15
  });
16
 
17
+ // 2. Optional: Logtail transport (if token is provided)
18
+ if (process.env.LOGTAIL_TOKEN) {
19
+ transports.push({
20
+ target: '@logtail/pino',
21
+ options: { sourceToken: process.env.LOGTAIL_TOKEN },
22
+ level: 'info'
23
+ });
24
  }
25
 
26
+ export const logger = pino({
27
+ level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
28
+ base: {
29
+ env: process.env.NODE_ENV,
30
+ service: 'api-gateway'
31
+ }
32
+ }, pino.multistream(transports));
33
+
34
+ // Compatibility with legacy logger usage if any
35
+ export const log = logger;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
apps/api/src/services/cleanup.ts CHANGED
@@ -3,22 +3,26 @@ import fs from 'fs/promises';
3
  import path from 'path';
4
  import cron from 'node-cron';
5
 
 
 
 
 
6
  /**
7
  * Cleanup Service
8
  *
9
- * Scans /tmp for old media files and deletes them to prevent disk saturation.
10
  */
11
- export async function cleanTempFiles(maxAgeMs: number = 30 * 60 * 1000) {
12
- const tempDir = '/tmp';
13
  const now = Date.now();
14
 
15
  try {
16
  const folders = ['', 'audio', 'images', 'documents'];
 
 
17
 
18
  for (const folder of folders) {
19
- const dirPath = path.join(tempDir, folder);
20
 
21
- // Check if directory exists
22
  try {
23
  await fs.access(dirPath);
24
  } catch {
@@ -29,31 +33,59 @@ export async function cleanTempFiles(maxAgeMs: number = 30 * 60 * 1000) {
29
 
30
  for (const file of files) {
31
  const filePath = path.join(dirPath, file);
32
-
33
- // Skip directories
34
  const stats = await fs.stat(filePath);
 
35
  if (stats.isDirectory()) continue;
36
 
37
- // Check age
38
  const age = now - stats.mtimeMs;
39
- if (age > maxAgeMs) {
40
  await fs.unlink(filePath);
41
- logger.info(`[CLEANUP] Deleted old temp file: ${filePath} (${Math.round(age / 60000)} min old)`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
 
43
  }
44
  }
 
45
  } catch (err: unknown) {
46
- logger.error(`[CLEANUP] Error during temp file cleanup:`, (err instanceof Error ? err.message : String(err)));
47
  }
48
  }
49
 
50
  /**
51
- * Starts a cron job that runs every hour to clean up /tmp.
52
  */
53
  export function startCleanupCron() {
54
- // Run every hour at minute 0
55
- cron.schedule('0 * * * *', () => {
56
- logger.info('[CLEANUP] 🧹 Starting scheduled temp file cleanup...');
57
  cleanTempFiles();
58
  });
59
 
 
3
  import path from 'path';
4
  import cron from 'node-cron';
5
 
6
+ const TEMP_DIR = '/tmp';
7
+ const GLOBAL_MAX_AGE = 30 * 60 * 1000; // 30 minutes
8
+ const PER_TENANT_QUOTA_MB = 100; // 100MB per tenant limit
9
+
10
  /**
11
  * Cleanup Service
12
  *
13
+ * Manages /tmp storage by enforcing age limits and per-tenant quotas.
14
  */
15
+ export async function cleanTempFiles() {
 
16
  const now = Date.now();
17
 
18
  try {
19
  const folders = ['', 'audio', 'images', 'documents'];
20
+ const tenantUsage: Record<string, number> = {};
21
+ const tenantFiles: Record<string, { path: string, size: number, mtime: number }[]> = {};
22
 
23
  for (const folder of folders) {
24
+ const dirPath = path.join(TEMP_DIR, folder);
25
 
 
26
  try {
27
  await fs.access(dirPath);
28
  } catch {
 
33
 
34
  for (const file of files) {
35
  const filePath = path.join(dirPath, file);
 
 
36
  const stats = await fs.stat(filePath);
37
+
38
  if (stats.isDirectory()) continue;
39
 
40
+ // 1. Check Age (Global cleanup)
41
  const age = now - stats.mtimeMs;
42
+ if (age > GLOBAL_MAX_AGE) {
43
  await fs.unlink(filePath);
44
+ logger.info(`[CLEANUP] Deleted old file (Age): ${filePath}`);
45
+ continue;
46
+ }
47
+
48
+ // 2. Track usage by tenant (Assumes files are named orgId_filename)
49
+ const orgId = file.split('_')[0];
50
+ if (orgId && orgId.length > 10) { // Basic orgId check
51
+ tenantUsage[orgId] = (tenantUsage[orgId] || 0) + stats.size;
52
+ if (!tenantFiles[orgId]) tenantFiles[orgId] = [];
53
+ tenantFiles[orgId].push({ path: filePath, size: stats.size, mtime: stats.mtimeMs });
54
+ }
55
+ }
56
+ }
57
+
58
+ // 3. Enforce Quotas
59
+ for (const orgId in tenantUsage) {
60
+ const usageMB = tenantUsage[orgId] / (1024 * 1024);
61
+ if (usageMB > PER_TENANT_QUOTA_MB) {
62
+ logger.warn(`[CLEANUP] Quota exceeded for Org ${orgId}: ${usageMB.toFixed(2)}MB. Cleaning oldest files...`);
63
+
64
+ // Sort by mtime (oldest first)
65
+ const files = tenantFiles[orgId].sort((a, b) => a.mtime - b.mtime);
66
+ let freed = 0;
67
+ const targetToFree = (usageMB - (PER_TENANT_QUOTA_MB * 0.8)) * 1024 * 1024; // Free up to 80% of quota
68
+
69
+ for (const file of files) {
70
+ await fs.unlink(file.path);
71
+ freed += file.size;
72
+ if (freed >= targetToFree) break;
73
  }
74
+ logger.info(`[CLEANUP] Freed ${(freed / (1024 * 1024)).toFixed(2)}MB for Org ${orgId}`);
75
  }
76
  }
77
+
78
  } catch (err: unknown) {
79
+ logger.error(`[CLEANUP] Error during cleanup:`, (err instanceof Error ? err.message : String(err)));
80
  }
81
  }
82
 
83
  /**
84
+ * Starts a cron job that runs every 30 minutes.
85
  */
86
  export function startCleanupCron() {
87
+ cron.schedule('*/30 * * * *', () => {
88
+ logger.info('[CLEANUP] 🧹 Starting scheduled maintenance...');
 
89
  cleanTempFiles();
90
  });
91
 
pnpm-lock.yaml CHANGED
@@ -118,6 +118,12 @@ importers:
118
  '@google/generative-ai':
119
  specifier: ^0.24.1
120
  version: 0.24.1
 
 
 
 
 
 
121
  '@prisma/client':
122
  specifier: ^5.0.0
123
  version: 5.22.0(prisma@5.22.0)
@@ -166,6 +172,9 @@ importers:
166
  openai:
167
  specifier: ^4.0.0
168
  version: 4.104.0(ws@8.19.0)(zod@3.25.76)
 
 
 
169
  pptxgenjs:
170
  specifier: ^3.12.0
171
  version: 3.12.0
@@ -1300,10 +1309,31 @@ packages:
1300
  '@jridgewell/trace-mapping@0.3.31':
1301
  resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
1302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1303
  '@lukeed/ms@2.0.2':
1304
  resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
1305
  engines: {node: '>=8'}
1306
 
 
 
 
 
1307
  '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
1308
  resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
1309
  cpu: [arm64]
@@ -1929,6 +1959,9 @@ packages:
1929
  '@types/react@18.3.28':
1930
  resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==}
1931
 
 
 
 
1932
  '@types/use-sync-external-store@0.0.6':
1933
  resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
1934
 
@@ -2227,6 +2260,9 @@ packages:
2227
  buffer@5.7.1:
2228
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
2229
 
 
 
 
2230
  bullmq@4.18.3:
2231
  resolution: {integrity: sha512-H8t9vhfHEbJDaXp7aalSTe+Do+tR1nvr+lsT+jQxLhy+FFfFj/0p4aYJzADTNLdEqltuxneLVxCGVg92GkQx4w==}
2232
 
@@ -2671,6 +2707,10 @@ packages:
2671
  events-universal@1.0.1:
2672
  resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
2673
 
 
 
 
 
2674
  execa@8.0.1:
2675
  resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
2676
  engines: {node: '>=16.17'}
@@ -3231,6 +3271,10 @@ packages:
3231
  resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
3232
  engines: {node: '>=10'}
3233
 
 
 
 
 
3234
  minimist@1.2.8:
3235
  resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
3236
 
@@ -3488,6 +3532,9 @@ packages:
3488
  resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
3489
  engines: {node: '>=0.10.0'}
3490
 
 
 
 
3491
  pino-abstract-transport@2.0.0:
3492
  resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
3493
 
@@ -3727,6 +3774,10 @@ packages:
3727
  readable-stream@2.3.8:
3728
  resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
3729
 
 
 
 
 
3730
  readdirp@3.6.0:
3731
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
3732
  engines: {node: '>=8.10.0'}
@@ -3870,6 +3921,10 @@ packages:
3870
  engines: {node: '>=10'}
3871
  hasBin: true
3872
 
 
 
 
 
3873
  set-cookie-parser@2.7.2:
3874
  resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
3875
 
@@ -3945,6 +4000,9 @@ packages:
3945
  resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
3946
  engines: {node: '>=0.8'}
3947
 
 
 
 
3948
  stackback@0.0.2:
3949
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
3950
 
@@ -3971,6 +4029,9 @@ packages:
3971
  string_decoder@1.1.1:
3972
  resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
3973
 
 
 
 
3974
  strip-ansi@6.0.1:
3975
  resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
3976
  engines: {node: '>=8'}
@@ -4154,6 +4215,10 @@ packages:
4154
  resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
4155
  engines: {node: '>=4'}
4156
 
 
 
 
 
4157
  typescript@5.9.3:
4158
  resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
4159
  engines: {node: '>=14.17'}
@@ -5502,8 +5567,38 @@ snapshots:
5502
  '@jridgewell/resolve-uri': 3.1.2
5503
  '@jridgewell/sourcemap-codec': 1.5.5
5504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5505
  '@lukeed/ms@2.0.2': {}
5506
 
 
 
5507
  '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
5508
  optional: true
5509
 
@@ -6217,6 +6312,8 @@ snapshots:
6217
  '@types/prop-types': 15.7.15
6218
  csstype: 3.2.3
6219
 
 
 
6220
  '@types/use-sync-external-store@0.0.6': {}
6221
 
6222
  '@types/yauzl@2.10.3':
@@ -6520,6 +6617,11 @@ snapshots:
6520
  base64-js: 1.5.1
6521
  ieee754: 1.2.1
6522
 
 
 
 
 
 
6523
  bullmq@4.18.3:
6524
  dependencies:
6525
  cron-parser: 4.9.0
@@ -6998,6 +7100,8 @@ snapshots:
6998
  transitivePeerDependencies:
6999
  - bare-abort-controller
7000
 
 
 
7001
  execa@8.0.1:
7002
  dependencies:
7003
  cross-spawn: 7.0.6
@@ -7569,6 +7673,10 @@ snapshots:
7569
  dependencies:
7570
  brace-expansion: 2.0.2
7571
 
 
 
 
 
7572
  minimist@1.2.8: {}
7573
 
7574
  minipass@7.1.3: {}
@@ -7815,6 +7923,11 @@ snapshots:
7815
 
7816
  pify@2.3.0: {}
7817
 
 
 
 
 
 
7818
  pino-abstract-transport@2.0.0:
7819
  dependencies:
7820
  split2: 4.2.0
@@ -8162,6 +8275,14 @@ snapshots:
8162
  string_decoder: 1.1.1
8163
  util-deprecate: 1.0.2
8164
 
 
 
 
 
 
 
 
 
8165
  readdirp@3.6.0:
8166
  dependencies:
8167
  picomatch: 2.3.1
@@ -8313,6 +8434,10 @@ snapshots:
8313
 
8314
  semver@7.7.4: {}
8315
 
 
 
 
 
8316
  set-cookie-parser@2.7.2: {}
8317
 
8318
  setimmediate@1.0.5: {}
@@ -8406,6 +8531,8 @@ snapshots:
8406
  dependencies:
8407
  frac: 1.1.2
8408
 
 
 
8409
  stackback@0.0.2: {}
8410
 
8411
  standard-as-callback@2.1.0: {}
@@ -8441,6 +8568,10 @@ snapshots:
8441
  dependencies:
8442
  safe-buffer: 5.1.2
8443
 
 
 
 
 
8444
  strip-ansi@6.0.1:
8445
  dependencies:
8446
  ansi-regex: 5.0.1
@@ -8638,6 +8769,8 @@ snapshots:
8638
 
8639
  type-detect@4.1.0: {}
8640
 
 
 
8641
  typescript@5.9.3: {}
8642
 
8643
  ufo@1.6.3: {}
 
118
  '@google/generative-ai':
119
  specifier: ^0.24.1
120
  version: 0.24.1
121
+ '@logtail/node':
122
+ specifier: ^0.5.8
123
+ version: 0.5.8
124
+ '@logtail/pino':
125
+ specifier: ^0.5.8
126
+ version: 0.5.8(pino@10.3.1)
127
  '@prisma/client':
128
  specifier: ^5.0.0
129
  version: 5.22.0(prisma@5.22.0)
 
172
  openai:
173
  specifier: ^4.0.0
174
  version: 4.104.0(ws@8.19.0)(zod@3.25.76)
175
+ pino-pretty:
176
+ specifier: ^13.1.3
177
+ version: 13.1.3
178
  pptxgenjs:
179
  specifier: ^3.12.0
180
  version: 3.12.0
 
1309
  '@jridgewell/trace-mapping@0.3.31':
1310
  resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
1311
 
1312
+ '@logtail/core@0.5.8':
1313
+ resolution: {integrity: sha512-1LxjI46LCXeVRyENwjO4s0a1CWSIqHjYH6w2YVNhQLc5b7mVN0jhzxvmg9mps54aEuCSuK2SVCNmgvuNTDtAug==}
1314
+
1315
+ '@logtail/node@0.5.8':
1316
+ resolution: {integrity: sha512-9XMxeToOL1QHVNp7IQlPxbRsu3iYUBtXl/SFCyOQQmtnwzS7z+tbiV39DYkXfwkzig6wjwTfLYDQj673Bqplqg==}
1317
+
1318
+ '@logtail/pino@0.5.8':
1319
+ resolution: {integrity: sha512-MRRAAn0APedr0v3uP91YnLJT1dJts/PGSRaBkFTyeG3gS28TkWbQc7ZruD17pDKEgOhRNJkj7c2v9uh8AhKFug==}
1320
+ peerDependencies:
1321
+ pino: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0
1322
+
1323
+ '@logtail/tools@0.5.8':
1324
+ resolution: {integrity: sha512-eAbbHaYSpRoB7Udp301xVUVDeKdgXTP2/s9FOeKX5tPLMM3hgbvuIx50bYJoV/to7bHGYBi36Gdsnj8ElcPXbg==}
1325
+
1326
+ '@logtail/types@0.5.8':
1327
+ resolution: {integrity: sha512-CJIijD/2vqK8XwPRZJeUkiiBEhKTviHs5p1WI40WvW21kU18QiQ8PupFJ2uhrItxhwtlkXmRXul5+cNtv3cAFQ==}
1328
+
1329
  '@lukeed/ms@2.0.2':
1330
  resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
1331
  engines: {node: '>=8'}
1332
 
1333
+ '@msgpack/msgpack@2.8.0':
1334
+ resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
1335
+ engines: {node: '>= 10'}
1336
+
1337
  '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
1338
  resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
1339
  cpu: [arm64]
 
1959
  '@types/react@18.3.28':
1960
  resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==}
1961
 
1962
+ '@types/stack-trace@0.0.33':
1963
+ resolution: {integrity: sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==}
1964
+
1965
  '@types/use-sync-external-store@0.0.6':
1966
  resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
1967
 
 
2260
  buffer@5.7.1:
2261
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
2262
 
2263
+ buffer@6.0.3:
2264
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
2265
+
2266
  bullmq@4.18.3:
2267
  resolution: {integrity: sha512-H8t9vhfHEbJDaXp7aalSTe+Do+tR1nvr+lsT+jQxLhy+FFfFj/0p4aYJzADTNLdEqltuxneLVxCGVg92GkQx4w==}
2268
 
 
2707
  events-universal@1.0.1:
2708
  resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
2709
 
2710
+ events@3.3.0:
2711
+ resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
2712
+ engines: {node: '>=0.8.x'}
2713
+
2714
  execa@8.0.1:
2715
  resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
2716
  engines: {node: '>=16.17'}
 
3271
  resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
3272
  engines: {node: '>=10'}
3273
 
3274
+ minimatch@9.0.9:
3275
+ resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
3276
+ engines: {node: '>=16 || 14 >=14.17'}
3277
+
3278
  minimist@1.2.8:
3279
  resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
3280
 
 
3532
  resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
3533
  engines: {node: '>=0.10.0'}
3534
 
3535
+ pino-abstract-transport@1.2.0:
3536
+ resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
3537
+
3538
  pino-abstract-transport@2.0.0:
3539
  resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
3540
 
 
3774
  readable-stream@2.3.8:
3775
  resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
3776
 
3777
+ readable-stream@4.7.0:
3778
+ resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
3779
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
3780
+
3781
  readdirp@3.6.0:
3782
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
3783
  engines: {node: '>=8.10.0'}
 
3921
  engines: {node: '>=10'}
3922
  hasBin: true
3923
 
3924
+ serialize-error@8.1.0:
3925
+ resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==}
3926
+ engines: {node: '>=10'}
3927
+
3928
  set-cookie-parser@2.7.2:
3929
  resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
3930
 
 
4000
  resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
4001
  engines: {node: '>=0.8'}
4002
 
4003
+ stack-trace@0.0.10:
4004
+ resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
4005
+
4006
  stackback@0.0.2:
4007
  resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
4008
 
 
4029
  string_decoder@1.1.1:
4030
  resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
4031
 
4032
+ string_decoder@1.3.0:
4033
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
4034
+
4035
  strip-ansi@6.0.1:
4036
  resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
4037
  engines: {node: '>=8'}
 
4215
  resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
4216
  engines: {node: '>=4'}
4217
 
4218
+ type-fest@0.20.2:
4219
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
4220
+ engines: {node: '>=10'}
4221
+
4222
  typescript@5.9.3:
4223
  resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
4224
  engines: {node: '>=14.17'}
 
5567
  '@jridgewell/resolve-uri': 3.1.2
5568
  '@jridgewell/sourcemap-codec': 1.5.5
5569
 
5570
+ '@logtail/core@0.5.8':
5571
+ dependencies:
5572
+ '@logtail/tools': 0.5.8
5573
+ '@logtail/types': 0.5.8
5574
+ serialize-error: 8.1.0
5575
+
5576
+ '@logtail/node@0.5.8':
5577
+ dependencies:
5578
+ '@logtail/core': 0.5.8
5579
+ '@logtail/types': 0.5.8
5580
+ '@msgpack/msgpack': 2.8.0
5581
+ '@types/stack-trace': 0.0.33
5582
+ minimatch: 9.0.9
5583
+ stack-trace: 0.0.10
5584
+
5585
+ '@logtail/pino@0.5.8(pino@10.3.1)':
5586
+ dependencies:
5587
+ '@logtail/node': 0.5.8
5588
+ '@logtail/types': 0.5.8
5589
+ pino: 10.3.1
5590
+ pino-abstract-transport: 1.2.0
5591
+
5592
+ '@logtail/tools@0.5.8':
5593
+ dependencies:
5594
+ '@logtail/types': 0.5.8
5595
+
5596
+ '@logtail/types@0.5.8': {}
5597
+
5598
  '@lukeed/ms@2.0.2': {}
5599
 
5600
+ '@msgpack/msgpack@2.8.0': {}
5601
+
5602
  '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
5603
  optional: true
5604
 
 
6312
  '@types/prop-types': 15.7.15
6313
  csstype: 3.2.3
6314
 
6315
+ '@types/stack-trace@0.0.33': {}
6316
+
6317
  '@types/use-sync-external-store@0.0.6': {}
6318
 
6319
  '@types/yauzl@2.10.3':
 
6617
  base64-js: 1.5.1
6618
  ieee754: 1.2.1
6619
 
6620
+ buffer@6.0.3:
6621
+ dependencies:
6622
+ base64-js: 1.5.1
6623
+ ieee754: 1.2.1
6624
+
6625
  bullmq@4.18.3:
6626
  dependencies:
6627
  cron-parser: 4.9.0
 
7100
  transitivePeerDependencies:
7101
  - bare-abort-controller
7102
 
7103
+ events@3.3.0: {}
7104
+
7105
  execa@8.0.1:
7106
  dependencies:
7107
  cross-spawn: 7.0.6
 
7673
  dependencies:
7674
  brace-expansion: 2.0.2
7675
 
7676
+ minimatch@9.0.9:
7677
+ dependencies:
7678
+ brace-expansion: 2.0.2
7679
+
7680
  minimist@1.2.8: {}
7681
 
7682
  minipass@7.1.3: {}
 
7923
 
7924
  pify@2.3.0: {}
7925
 
7926
+ pino-abstract-transport@1.2.0:
7927
+ dependencies:
7928
+ readable-stream: 4.7.0
7929
+ split2: 4.2.0
7930
+
7931
  pino-abstract-transport@2.0.0:
7932
  dependencies:
7933
  split2: 4.2.0
 
8275
  string_decoder: 1.1.1
8276
  util-deprecate: 1.0.2
8277
 
8278
+ readable-stream@4.7.0:
8279
+ dependencies:
8280
+ abort-controller: 3.0.0
8281
+ buffer: 6.0.3
8282
+ events: 3.3.0
8283
+ process: 0.11.10
8284
+ string_decoder: 1.3.0
8285
+
8286
  readdirp@3.6.0:
8287
  dependencies:
8288
  picomatch: 2.3.1
 
8434
 
8435
  semver@7.7.4: {}
8436
 
8437
+ serialize-error@8.1.0:
8438
+ dependencies:
8439
+ type-fest: 0.20.2
8440
+
8441
  set-cookie-parser@2.7.2: {}
8442
 
8443
  setimmediate@1.0.5: {}
 
8531
  dependencies:
8532
  frac: 1.1.2
8533
 
8534
+ stack-trace@0.0.10: {}
8535
+
8536
  stackback@0.0.2: {}
8537
 
8538
  standard-as-callback@2.1.0: {}
 
8568
  dependencies:
8569
  safe-buffer: 5.1.2
8570
 
8571
+ string_decoder@1.3.0:
8572
+ dependencies:
8573
+ safe-buffer: 5.2.1
8574
+
8575
  strip-ansi@6.0.1:
8576
  dependencies:
8577
  ansi-regex: 5.0.1
 
8769
 
8770
  type-detect@4.1.0: {}
8771
 
8772
+ type-fest@0.20.2: {}
8773
+
8774
  typescript@5.9.3: {}
8775
 
8776
  ufo@1.6.3: {}