Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
217 lines
7.8 KiB
JavaScript
217 lines
7.8 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getTenantDatabasePath = getTenantDatabasePath;
|
|
exports.getTenantChromaCollection = getTenantChromaCollection;
|
|
exports.getTenantRedisPrefix = getTenantRedisPrefix;
|
|
exports.getTenantMinioBucket = getTenantMinioBucket;
|
|
exports.getSQLiteWALConfig = getSQLiteWALConfig;
|
|
exports.getSQLiteEncryptionConfig = getSQLiteEncryptionConfig;
|
|
exports.getTenantDatabaseSchema = getTenantDatabaseSchema;
|
|
exports.generateDocumentChunkId = generateDocumentChunkId;
|
|
exports.parseConnectionString = parseConnectionString;
|
|
exports.escapeSQLIdentifier = escapeSQLIdentifier;
|
|
exports.generateBackupFilename = generateBackupFilename;
|
|
// Database utility functions
|
|
const path_1 = __importDefault(require("path"));
|
|
const crypto_1 = __importDefault(require("crypto"));
|
|
/**
|
|
* Generate SQLite database path for tenant
|
|
*/
|
|
function getTenantDatabasePath(tenantDomain, dataDir = '/data') {
|
|
return path_1.default.join(dataDir, tenantDomain, 'app.db');
|
|
}
|
|
/**
|
|
* Generate ChromaDB collection name for tenant
|
|
*/
|
|
function getTenantChromaCollection(tenantDomain) {
|
|
// ChromaDB collection names must be alphanumeric with underscores
|
|
return `gt2_${tenantDomain.replace(/-/g, '_')}_documents`;
|
|
}
|
|
/**
|
|
* Generate Redis key prefix for tenant
|
|
*/
|
|
function getTenantRedisPrefix(tenantDomain) {
|
|
return `gt2:${tenantDomain}:`;
|
|
}
|
|
/**
|
|
* Generate MinIO bucket name for tenant
|
|
*/
|
|
function getTenantMinioBucket(tenantDomain) {
|
|
// MinIO bucket names must be lowercase and DNS-compliant
|
|
return `gt2-${tenantDomain}-files`;
|
|
}
|
|
/**
|
|
* Generate SQLite WAL mode configuration
|
|
*/
|
|
function getSQLiteWALConfig() {
|
|
return `
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA synchronous=NORMAL;
|
|
PRAGMA cache_size=1000;
|
|
PRAGMA foreign_keys=ON;
|
|
PRAGMA temp_store=MEMORY;
|
|
`;
|
|
}
|
|
/**
|
|
* Generate SQLite encryption configuration
|
|
*/
|
|
function getSQLiteEncryptionConfig(encryptionKey) {
|
|
return `PRAGMA key='${encryptionKey}';`;
|
|
}
|
|
/**
|
|
* Create tenant database schema (SQLite)
|
|
*/
|
|
function getTenantDatabaseSchema() {
|
|
return `
|
|
-- Enable foreign key constraints
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
-- Conversations for AI chat
|
|
CREATE TABLE IF NOT EXISTS conversations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
model_id TEXT NOT NULL,
|
|
system_prompt TEXT,
|
|
created_by TEXT NOT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Messages with full context tracking
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
|
content TEXT NOT NULL,
|
|
model_used TEXT,
|
|
tokens_used INTEGER DEFAULT 0,
|
|
context_sources TEXT DEFAULT '[]', -- JSON array of document chunk IDs
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Documents with processing status
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
filename TEXT NOT NULL,
|
|
file_type TEXT NOT NULL,
|
|
file_size INTEGER DEFAULT 0,
|
|
processing_status TEXT DEFAULT 'pending' CHECK (processing_status IN ('pending', 'processing', 'completed', 'failed')),
|
|
chunk_count INTEGER DEFAULT 0,
|
|
uploaded_by TEXT NOT NULL,
|
|
storage_path TEXT,
|
|
error_message TEXT,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Document chunks for RAG
|
|
CREATE TABLE IF NOT EXISTS document_chunks (
|
|
id TEXT PRIMARY KEY, -- UUID
|
|
document_id INTEGER NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
chunk_index INTEGER NOT NULL,
|
|
content TEXT NOT NULL,
|
|
metadata TEXT DEFAULT '{}', -- JSON metadata
|
|
embedding_id TEXT, -- Reference to ChromaDB embedding
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- User sessions and preferences
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
id TEXT PRIMARY KEY, -- Session token
|
|
user_email TEXT NOT NULL,
|
|
expires_at DATETIME NOT NULL,
|
|
data TEXT DEFAULT '{}', -- JSON session data
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- User preferences
|
|
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
user_email TEXT PRIMARY KEY,
|
|
preferences TEXT DEFAULT '{}', -- JSON preferences
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Usage tracking for tenant
|
|
CREATE TABLE IF NOT EXISTS usage_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_email TEXT NOT NULL,
|
|
action_type TEXT NOT NULL, -- 'chat', 'document_upload', 'document_query'
|
|
resource_used TEXT, -- Model name or resource identifier
|
|
tokens_used INTEGER DEFAULT 0,
|
|
success BOOLEAN DEFAULT TRUE,
|
|
metadata TEXT DEFAULT '{}', -- JSON metadata
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
-- Indexes for performance
|
|
CREATE INDEX IF NOT EXISTS idx_conversations_created_by ON conversations(created_by);
|
|
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_documents_uploaded_by ON documents(uploaded_by);
|
|
CREATE INDEX IF NOT EXISTS idx_documents_status ON documents(processing_status);
|
|
CREATE INDEX IF NOT EXISTS idx_document_chunks_document_id ON document_chunks(document_id);
|
|
CREATE INDEX IF NOT EXISTS idx_usage_logs_user_email ON usage_logs(user_email);
|
|
CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
|
|
|
|
-- Triggers for updated_at columns
|
|
CREATE TRIGGER IF NOT EXISTS update_conversations_updated_at
|
|
AFTER UPDATE ON conversations
|
|
BEGIN
|
|
UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS update_documents_updated_at
|
|
AFTER UPDATE ON documents
|
|
BEGIN
|
|
UPDATE documents SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
END;
|
|
|
|
CREATE TRIGGER IF NOT EXISTS update_user_preferences_updated_at
|
|
AFTER UPDATE ON user_preferences
|
|
BEGIN
|
|
UPDATE user_preferences SET updated_at = CURRENT_TIMESTAMP WHERE user_email = NEW.user_email;
|
|
END;
|
|
`;
|
|
}
|
|
/**
|
|
* Generate unique document chunk ID
|
|
*/
|
|
function generateDocumentChunkId(documentId, chunkIndex) {
|
|
const data = `${documentId}-${chunkIndex}-${Date.now()}`;
|
|
return crypto_1.default.createHash('sha256').update(data).digest('hex').substring(0, 32);
|
|
}
|
|
/**
|
|
* Parse connection string for database configuration
|
|
*/
|
|
function parseConnectionString(connectionString) {
|
|
const url = new URL(connectionString);
|
|
return {
|
|
host: url.hostname,
|
|
port: url.port ? parseInt(url.port) : undefined,
|
|
database: url.pathname.substring(1), // Remove leading slash
|
|
username: url.username,
|
|
password: url.password,
|
|
options: Object.fromEntries(url.searchParams.entries())
|
|
};
|
|
}
|
|
/**
|
|
* Escape SQL identifiers (table names, column names, etc.)
|
|
*/
|
|
function escapeSQLIdentifier(identifier) {
|
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
}
|
|
/**
|
|
* Generate database backup filename
|
|
*/
|
|
function generateBackupFilename(tenantDomain, timestamp) {
|
|
const date = timestamp || new Date();
|
|
const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
const timeString = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS
|
|
return `gt2-${tenantDomain}-backup-${dateString}-${timeString}.db`;
|
|
}
|