UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

359 lines (358 loc) 11 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { v4 as uuidv4 } from "uuid"; import * as bcrypt from "bcryptjs"; import { logger } from "../core/monitoring/logger.js"; class UserModel { db; constructor(db) { this.db = db; this.initialize(); } initialize() { this.db.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, sub TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, name TEXT, avatar TEXT, tier TEXT DEFAULT 'free', permissions TEXT DEFAULT '["read", "write"]', organizations TEXT DEFAULT '[]', api_keys TEXT DEFAULT '[]', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login_at DATETIME, metadata TEXT DEFAULT '{}' ) `); this.db.exec(` CREATE TABLE IF NOT EXISTS user_sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, token TEXT UNIQUE NOT NULL, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, metadata TEXT DEFAULT '{}', FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); this.db.exec(` CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, key_hash TEXT UNIQUE NOT NULL, name TEXT, last_used_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME, metadata TEXT DEFAULT '{}', FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) `); this.db.exec(` CREATE INDEX IF NOT EXISTS idx_users_sub ON users(sub); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token); CREATE INDEX IF NOT EXISTS idx_sessions_user ON user_sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON user_sessions(expires_at); CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash); CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id); `); logger.info("User database schema initialized"); } async createUser(userData) { if (!userData.sub || !userData.email) { throw new Error("User sub and email are required"); } const user = { id: userData.id || uuidv4(), sub: userData.sub, email: userData.email, name: userData.name, avatar: userData.avatar, tier: userData.tier || "free", permissions: userData.permissions || ["read", "write"], organizations: userData.organizations || [], apiKeys: userData.apiKeys || [], createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), metadata: userData.metadata || {} }; const stmt = this.db.prepare(` INSERT INTO users ( id, sub, email, name, avatar, tier, permissions, organizations, api_keys, created_at, updated_at, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( user.id, user.sub, user.email, user.name, user.avatar, user.tier, JSON.stringify(user.permissions), JSON.stringify(user.organizations), JSON.stringify(user.apiKeys), user.createdAt.toISOString(), user.updatedAt.toISOString(), JSON.stringify(user.metadata) ); logger.info("User created", { userId: user.id, email: user.email }); return user; } async findUserBySub(sub) { const stmt = this.db.prepare("SELECT * FROM users WHERE sub = ?"); const row = stmt.get(sub); if (!row) { return null; } return this.rowToUser(row); } async findUserByEmail(email) { const stmt = this.db.prepare("SELECT * FROM users WHERE email = ?"); const row = stmt.get(email); if (!row) { return null; } return this.rowToUser(row); } async findUserById(id) { const stmt = this.db.prepare("SELECT * FROM users WHERE id = ?"); const row = stmt.get(id); if (!row) { return null; } return this.rowToUser(row); } async updateUser(id, updates) { const user = await this.findUserById(id); if (!user) { return null; } const updatedUser = { ...user, ...updates, updatedAt: /* @__PURE__ */ new Date() }; const stmt = this.db.prepare(` UPDATE users SET email = ?, name = ?, avatar = ?, tier = ?, permissions = ?, organizations = ?, api_keys = ?, updated_at = ?, last_login_at = ?, metadata = ? WHERE id = ? `); stmt.run( updatedUser.email, updatedUser.name, updatedUser.avatar, updatedUser.tier, JSON.stringify(updatedUser.permissions), JSON.stringify(updatedUser.organizations), JSON.stringify(updatedUser.apiKeys), updatedUser.updatedAt.toISOString(), updatedUser.lastLoginAt?.toISOString(), JSON.stringify(updatedUser.metadata), id ); logger.info("User updated", { userId: id }); return updatedUser; } async deleteUser(id) { const stmt = this.db.prepare("DELETE FROM users WHERE id = ?"); const result = stmt.run(id); if (result.changes > 0) { logger.info("User deleted", { userId: id }); return true; } return false; } async updateLastLogin(id) { const stmt = this.db.prepare( "UPDATE users SET last_login_at = ? WHERE id = ?" ); stmt.run((/* @__PURE__ */ new Date()).toISOString(), id); } // Session management async createSession(userId, expiresIn = 86400) { const session = { id: uuidv4(), userId, token: this.generateSessionToken(), expiresAt: new Date(Date.now() + expiresIn * 1e3), createdAt: /* @__PURE__ */ new Date(), metadata: {} }; const stmt = this.db.prepare(` INSERT INTO user_sessions (id, user_id, token, expires_at, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?) `); stmt.run( session.id, session.userId, session.token, session.expiresAt.toISOString(), session.createdAt.toISOString(), JSON.stringify(session.metadata) ); logger.info("Session created", { sessionId: session.id, userId }); return session; } async findSessionByToken(token) { const stmt = this.db.prepare("SELECT * FROM user_sessions WHERE token = ?"); const row = stmt.get(token); if (!row) { return null; } return this.rowToSession(row); } async validateSession(token) { const session = await this.findSessionByToken(token); if (!session) { return null; } if (new Date(session.expiresAt) < /* @__PURE__ */ new Date()) { await this.deleteSession(session.id); return null; } return await this.findUserById(session.userId); } async deleteSession(id) { const stmt = this.db.prepare("DELETE FROM user_sessions WHERE id = ?"); const result = stmt.run(id); return result.changes > 0; } async deleteExpiredSessions() { const stmt = this.db.prepare( "DELETE FROM user_sessions WHERE expires_at < ?" ); const result = stmt.run((/* @__PURE__ */ new Date()).toISOString()); if (result.changes > 0) { logger.info("Expired sessions deleted", { count: result.changes }); } return result.changes; } // API Key management async generateApiKey(userId, name) { const user = await this.findUserById(userId); if (!user) { throw new Error("User not found"); } const apiKey = `sk-${this.generateToken(32)}`; const hashedKey = await bcrypt.hash(apiKey, 10); const stmt = this.db.prepare(` INSERT INTO api_keys (id, user_id, key_hash, name, created_at) VALUES (?, ?, ?, ?, ?) `); const apiKeyId = uuidv4(); stmt.run( apiKeyId, userId, hashedKey, name || "API Key", (/* @__PURE__ */ new Date()).toISOString() ); logger.info("API key generated", { userId, apiKeyId }); return apiKey; } async validateApiKey(apiKey) { const stmt = this.db.prepare(` SELECT u.*, ak.id as api_key_id, ak.key_hash FROM api_keys ak JOIN users u ON ak.user_id = u.id WHERE (ak.expires_at IS NULL OR ak.expires_at > datetime('now')) `); const rows = stmt.all(); for (const row of rows) { if (await bcrypt.compare(apiKey, row.key_hash)) { const updateStmt = this.db.prepare( "UPDATE api_keys SET last_used_at = ? WHERE id = ?" ); updateStmt.run((/* @__PURE__ */ new Date()).toISOString(), row.api_key_id); return this.rowToUser(row); } } return null; } async revokeApiKey(userId, apiKeyId) { const stmt = this.db.prepare( "DELETE FROM api_keys WHERE id = ? AND user_id = ?" ); const result = stmt.run(apiKeyId, userId); if (result.changes > 0) { logger.info("API key revoked", { userId, apiKeyId }); return true; } return false; } async listApiKeys(userId) { const stmt = this.db.prepare(` SELECT id, name, last_used_at, created_at FROM api_keys WHERE user_id = ? ORDER BY created_at DESC `); const rows = stmt.all(userId); return rows.map((row) => ({ id: row.id, name: row.name, lastUsed: row.last_used_at ? new Date(row.last_used_at) : void 0, createdAt: new Date(row.created_at) })); } // Helper methods rowToUser(row) { return { id: row.id, sub: row.sub, email: row.email, name: row.name, avatar: row.avatar, tier: row.tier, permissions: JSON.parse(row.permissions), organizations: JSON.parse(row.organizations), apiKeys: JSON.parse(row.api_keys || "[]"), createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), lastLoginAt: row.last_login_at ? new Date(row.last_login_at) : void 0, metadata: JSON.parse(row.metadata || "{}") }; } rowToSession(row) { return { id: row.id, userId: row.user_id, token: row.token, expiresAt: new Date(row.expires_at), createdAt: new Date(row.created_at), metadata: JSON.parse(row.metadata || "{}") }; } generateSessionToken() { return this.generateToken(48); } generateToken(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let token = ""; for (let i = 0; i < length; i++) { token += chars.charAt(Math.floor(Math.random() * chars.length)); } return token; } } let userModelInstance = null; function getUserModel(db) { if (!userModelInstance) { userModelInstance = new UserModel(db); } return userModelInstance; } export { UserModel, getUserModel }; //# sourceMappingURL=user.model.js.map