File size: 4,093 Bytes
9179e11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba37a9b
9179e11
 
 
 
8bde444
 
 
 
 
 
 
 
ba37a9b
 
 
 
 
9179e11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ba37a9b
 
 
 
 
 
 
 
 
9179e11
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import 'dotenv/config';

export const config = {
  // Supabase
  supabaseUrl: process.env.SUPABASE_URL || '',
  supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY || '',

  // Worker
  workerId: process.env.WORKER_ID || `worker-${process.pid}`,
  maxWorkers: parseInt(process.env.MAX_WORKERS || '2', 10),
  pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '3000', 10),

  // Turnitin
  turnitinSharedPassword: process.env.TURNITIN_SHARED_PASSWORD || '',
  credentialEncryptionKey: process.env.TURNITIN_CREDENTIAL_ENCRYPTION_KEY || '',
  turnitinTargetUrl:
    process.env.TARGET_URL ||
    'https://www.turnitin.com/login_page.asp?lang=en_us',
  turnitinClassTitle: process.env.CLASS_TITLE || 'Summer Reading 2026',
  turnitinAssignmentTitle: process.env.ASSIGNMENT_TITLE || 'HTLLP notes',

  // Playwright
  headless: process.env.PLAYWRIGHT_HEADLESS !== 'false',

  // Storage buckets
  storageProvider: process.env.STORAGE_PROVIDER === 'r2' ? 'r2' : 'supabase',
  inputBucket: process.env.INPUT_BUCKET || 'turnitin-inputs',
  reportBucket: process.env.REPORT_BUCKET || 'turnitin-reports',
  diagnosticsBucket: process.env.DIAGNOSTICS_BUCKET || 'turnitin-diagnostics',
  sessionBucket: process.env.SESSION_BUCKET || 'turnitin-sessions',
  // Plain storage paths were created before R2 and must always resolve
  // against the original Supabase bucket names after the R2 switch.
  supabaseInputBucket:
    process.env.SUPABASE_INPUT_BUCKET || 'turnitin-inputs',
  supabaseReportBucket:
    process.env.SUPABASE_REPORT_BUCKET || 'turnitin-reports',
  supabaseSessionBucket:
    process.env.SUPABASE_SESSION_BUCKET || 'turnitin-sessions',
  r2AccountId: process.env.R2_ACCOUNT_ID || '',
  r2AccessKeyId: process.env.R2_ACCESS_KEY_ID || '',
  r2SecretAccessKey: process.env.R2_SECRET_ACCESS_KEY || '',
  r2Endpoint: process.env.R2_ENDPOINT || '',
  r2Region: process.env.R2_REGION || 'auto',

  // Cron intervals (minutes)
  quotaCheckInterval: parseInt(process.env.QUOTA_CHECK_INTERVAL || '30', 10),
  quotaCheckAccountDelayMs: parseInt(
    process.env.QUOTA_CHECK_ACCOUNT_DELAY_MS || String(5 * 60 * 1000),
    10,
  ),
  quotaCheckOrder: process.env.QUOTA_CHECK_ORDER === 'newest' ? 'newest' : 'oldest',
  cleanupInterval: parseInt(process.env.CLEANUP_INTERVAL || '15', 10),
  staleRecoveryInterval: parseInt(process.env.STALE_RECOVERY_INTERVAL || '5', 10),

  // Report
  reportRetentionHours: parseInt(process.env.REPORT_RETENTION_HOURS || '24', 10),

  // Server
  port: parseInt(process.env.PORT || '7860', 10),
  internalSecret: process.env.INTERNAL_SECRET || '',
  allowedOrigins: (process.env.ALLOWED_ORIGINS || process.env.FRONTEND_ORIGIN || '')
    .split(',')
    .map((origin) => origin.trim())
    .filter(Boolean),
  enableWorker: process.env.ENABLE_WORKER !== 'false',
  enableCron: process.env.ENABLE_CRON !== 'false',

  // Timeouts
  similarityTimeoutMs: parseInt(process.env.SIMILARITY_TIMEOUT_MS || '180000', 10),
  similarityPollMs: parseInt(process.env.SIMILARITY_POLL_MS || '5000', 10),
  similarityRefreshAfterMs: parseInt(process.env.SIMILARITY_REFRESH_AFTER_MS || '90000', 10),
  // BUG-10 FIX: leaseMinutes was hardcoded to 20 min — shorter than the combined
  // time for login + navigate + similarity wait (up to 30+ min). Stale recovery
  // could reset an account that is still being actively used.
  leaseMinutes: parseInt(process.env.LEASE_MINUTES || '35', 10),
} as const;

/** Validate required environment variables at startup */
export function validateConfig(): void {
  const required: (keyof typeof config)[] = [
    'supabaseUrl',
    'supabaseServiceRoleKey',
    'turnitinSharedPassword',
  ];

  const missing = required.filter((key) => !config[key]);
  if (config.storageProvider === 'r2') {
    const r2Required: (keyof typeof config)[] = [
      'r2AccountId',
      'r2AccessKeyId',
      'r2SecretAccessKey',
      'r2Endpoint',
    ];
    missing.push(...r2Required.filter((key) => !config[key]));
  }
  if (missing.length > 0) {
    throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
  }
}