import fs from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
const DB_DIR = path.join(process.cwd(), 'data');
const API_KEYS_FILE = path.join(DB_DIR, 'api_keys.json');
const LEADS_FILE = path.join(DB_DIR, 'leads.json');
const MESSAGES_FILE = path.join(DB_DIR, 'messages.json');
const SESSIONS_FILE = path.join(DB_DIR, 'sessions.json');
const USAGE_FILE = path.join(DB_DIR, 'usage.json');
const SEQUENCES_FILE = path.join(DB_DIR, 'sequences.json');
class Database {
constructor() {
this.initialized = false;
}
async init() {
if (!existsSync(DB_DIR)) {
await fs.mkdir(DB_DIR, { recursive: true });
}
const files = [API_KEYS_FILE, LEADS_FILE, MESSAGES_FILE, SESSIONS_FILE, USAGE_FILE, SEQUENCES_FILE];
for (const file of files) {
if (!existsSync(file)) {
await fs.writeFile(file, JSON.stringify([]), 'utf8');
}
}
this.initialized = true;
}
async readFile(filePath) {
const data = await fs.readFile(filePath, 'utf8');
return JSON.parse(data);
}
async writeFile(filePath, data) {
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
}
// API Key Management
async createApiKey(tier = 'starter') {
const apiKey = crypto.randomBytes(32).toString('hex');
const hash = await bcrypt.hash(apiKey, 10);
const keys = await this.readFile(API_KEYS_FILE);
const newKey = {
id: crypto.randomUUID(),
hash,
tier,
created_at: new Date().toISOString(),
last_used: null,
is_active: true
};
keys.push(newKey);
await this.writeFile(API_KEYS_FILE, keys);
return { id: newKey.id, key: apiKey, tier };
}
async validateApiKey(apiKey) {
const keys = await this.readFile(API_KEYS_FILE);
for (const key of keys) {
if (!key.is_active) continue;
const valid = await bcrypt.compare(apiKey, key.hash);
if (valid) {
key.last_used = new Date().toISOString();
await this.writeFile(API_KEYS_FILE, keys);
return key;
}
}
return null;
}
async getTierLimits(tier) {
const limits = {
starter: { profiles: 500, messages: 200, sequences: 2 },
professional: { profiles: 2000, messages: 1000, sequences: 10 },
agency: { profiles: 10000, messages: 5000, sequences: -1 },
enterprise: { profiles: -1, messages: -1, sequences: -1 }
};
return limits[tier] || limits.starter;
}
// Usage Tracking
async checkUsageLimit(userId, action) {
const usage = await this.readFile(USAGE_FILE);
const keys = await this.readFile(API_KEYS_FILE);
const key = keys.find(k => k.id === userId);
if (!key) return { allowed: false, reason: 'Invalid user' };
const limits = await this.getTierLimits(key.tier);
const currentMonth = new Date().toISOString().slice(0, 7);
let userUsage = usage.find(u => u.user_id === userId && u.month === currentMonth);
if (!userUsage) {
userUsage = { user_id: userId, month: currentMonth, profiles: 0, messages: 0, sequences: 0 };
usage.push(userUsage);
}
const actionMap = { profile_analysis: 'profiles', message_send: 'messages', sequence_create: 'sequences' };
const limitKey = actionMap[action];
if (limits[limitKey] !== -1 && userUsage[limitKey] >= limits[limitKey]) {
return { allowed: false, reason: `Monthly ${limitKey} limit reached` };
}
userUsage[limitKey]++;
await this.writeFile(USAGE_FILE, usage);
return { allowed: true, remaining: limits[limitKey] === -1 ? -1 : limits[limitKey] - userUsage[limitKey] };
}
// Session Management
async saveSession(userId, liAtCookie) {
const sessions = await this.readFile(SESSIONS_FILE);
let session = sessions.find(s => s.user_id === userId);
if (!session) {
session = { user_id: userId, cookies: [], created_at: new Date().toISOString() };
sessions.push(session);
}
session.cookies = [{ name: 'li_at', value: liAtCookie, domain: '.linkedin.com' }];
session.updated_at = new Date().toISOString();
session.is_valid = true;
await this.writeFile(SESSIONS_FILE, sessions);
return session;
}
async getSession(userId) {
const sessions = await this.readFile(SESSIONS_FILE);
return sessions.find(s => s.user_id === userId && s.is_valid);
}
async invalidateSession(userId) {
const sessions = await this.readFile(SESSIONS_FILE);
const session = sessions.find(s => s.user_id === userId);
if (session) {
session.is_valid = false;
await this.writeFile(SESSIONS_FILE, sessions);
}
}
// Lead Management
async saveLead(lead) {
const leads = await this.readFile(LEADS_FILE);
const existing = leads.find(l => l.profile_url === lead.profile_url);
if (existing) {
Object.assign(existing, lead, { updated_at: new Date().toISOString() });
} else {
lead.id = crypto.randomUUID();
lead.created_at = new Date().toISOString();
leads.push(lead);
}
await this.writeFile(LEADS_FILE, leads);
return lead;
}
async getLead(profileUrl) {
const leads = await this.readFile(LEADS_FILE);
return leads.find(l => l.profile_url === profileUrl);
}
async getLeads(filters = {}) {
let leads = await this.readFile(LEADS_FILE);
if (filters.min_score) {
leads = leads.filter(l => l.score >= filters.min_score);
}
if (filters.user_id) {
leads = leads.filter(l => l.user_id === filters.user_id);
}
return leads;
}
// Message Management
async saveMessage(message) {
const messages = await this.readFile(MESSAGES_FILE);
message.id = crypto.randomUUID();
message.created_at = new Date().toISOString();
messages.push(message);
await this.writeFile(MESSAGES_FILE, messages);
return message;
}
async getMessages(leadId) {
const messages = await this.readFile(MESSAGES_FILE);
return messages.filter(m => m.lead_id === leadId).sort((a, b) =>
new Date(a.created_at) - new Date(b.created_at)
);
}
async getPendingFollowups() {
const messages = await this.readFile(MESSAGES_FILE);
const sequences = await this.readFile(SEQUENCES_FILE);
const now = new Date();
const pending = [];
for (const seq of sequences) {
if (!seq.is_active) continue;
const leadMessages = messages.filter(m => m.lead_id === seq.lead_id);
const lastMessage = leadMessages[leadMessages.length - 1];
if (!lastMessage) continue;
const nextStage = lastMessage.sequence_stage + 1;
if (nextStage >= seq.messages.length) continue;
const daysSinceLastMessage = (now - new Date(lastMessage.sent_at)) / (1000 * 60 * 60 * 24);
const nextMessage = seq.messages[nextStage];
if (daysSinceLastMessage >= nextMessage.days_after) {
pending.push({ sequence: seq, lead_id: seq.lead_id, stage: nextStage, message: nextMessage });
}
}
return pending;
}
// Follow-up Sequence Management
async createSequence(sequence) {
const sequences = await this.readFile(SEQUENCES_FILE);
sequence.id = crypto.randomUUID();
sequence.created_at = new Date().toISOString();
sequence.is_active = true;
sequences.push(sequence);
await this.writeFile(SEQUENCES_FILE, sequences);
return sequence;
}
async getSequence(leadId) {
const sequences = await this.readFile(SEQUENCES_FILE);
return sequences.find(s => s.lead_id === leadId && s.is_active);
}
async updateSequence(sequenceId, updates) {
const sequences = await this.readFile(SEQUENCES_FILE);
const sequence = sequences.find(s => s.id === sequenceId);
if (sequence) {
Object.assign(sequence, updates, { updated_at: new Date().toISOString() });
await this.writeFile(SEQUENCES_FILE, sequences);
}
return sequence;
}
}
export default new Database();