GT AI OS Community Edition v2.0.33

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>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
/**
* Unit tests for authentication utilities
*/
import {
generateCapabilityHash,
verifyCapabilityHash,
createJWT,
verifyJWT,
hasCapability,
hashPassword,
verifyPassword,
generateSecureToken,
createTenantCapabilities,
createSuperAdminCapabilities,
extractBearerToken,
isTokenExpired
} from '../auth';
import { Capability } from '@gt2/types';
describe('Authentication Utilities', () => {
describe('Capability Hash Functions', () => {
const testCapabilities: Capability[] = [
{
resource: 'tenant:test:*',
actions: ['read', 'write'],
constraints: {}
}
];
test('generateCapabilityHash creates consistent hash', () => {
const hash1 = generateCapabilityHash(testCapabilities);
const hash2 = generateCapabilityHash(testCapabilities);
expect(hash1).toBe(hash2);
expect(typeof hash1).toBe('string');
expect(hash1.length).toBeGreaterThan(0);
});
test('verifyCapabilityHash validates correct hash', () => {
const hash = generateCapabilityHash(testCapabilities);
const isValid = verifyCapabilityHash(testCapabilities, hash);
expect(isValid).toBe(true);
});
test('verifyCapabilityHash rejects incorrect hash', () => {
const isValid = verifyCapabilityHash(testCapabilities, 'incorrect-hash');
expect(isValid).toBe(false);
});
test('capability hash changes with different capabilities', () => {
const capabilities1: Capability[] = [
{ resource: 'tenant:test1:*', actions: ['read'], constraints: {} }
];
const capabilities2: Capability[] = [
{ resource: 'tenant:test2:*', actions: ['write'], constraints: {} }
];
const hash1 = generateCapabilityHash(capabilities1);
const hash2 = generateCapabilityHash(capabilities2);
expect(hash1).not.toBe(hash2);
});
});
describe('JWT Functions', () => {
const testPayload = {
sub: 'test@example.com',
tenant_id: '123',
user_type: 'tenant_user' as const,
capabilities: [
{
resource: 'tenant:test:*',
actions: ['read', 'write'],
constraints: {}
}
]
};
test('createJWT generates valid token', () => {
const token = createJWT(testPayload);
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
});
test('verifyJWT validates correct token', () => {
const token = createJWT(testPayload);
const decoded = verifyJWT(token);
expect(decoded).toBeTruthy();
expect(decoded?.sub).toBe(testPayload.sub);
expect(decoded?.tenant_id).toBe(testPayload.tenant_id);
expect(decoded?.user_type).toBe(testPayload.user_type);
});
test('verifyJWT rejects invalid token', () => {
const decoded = verifyJWT('invalid.token.here');
expect(decoded).toBeNull();
});
test('verifyJWT rejects tampered token', () => {
const token = createJWT(testPayload);
const tamperedToken = token.slice(0, -10) + 'tampered123';
const decoded = verifyJWT(tamperedToken);
expect(decoded).toBeNull();
});
test('isTokenExpired detects expired tokens', () => {
const expiredPayload = {
...testPayload,
exp: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
iat: Math.floor(Date.now() / 1000) - 7200 // 2 hours ago
};
expect(isTokenExpired(expiredPayload as any)).toBe(true);
});
test('isTokenExpired allows valid tokens', () => {
const validPayload = {
...testPayload,
exp: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
iat: Math.floor(Date.now() / 1000) // Now
};
expect(isTokenExpired(validPayload as any)).toBe(false);
});
});
describe('Capability Authorization', () => {
const userCapabilities: Capability[] = [
{
resource: 'tenant:acme:*',
actions: ['read', 'write'],
constraints: {}
},
{
resource: 'ai_resource:*',
actions: ['use'],
constraints: {
usage_limits: {
max_requests_per_hour: 100
}
}
}
];
test('hasCapability grants access for exact match', () => {
const hasAccess = hasCapability(userCapabilities, 'tenant:acme:conversations', 'read');
expect(hasAccess).toBe(true);
});
test('hasCapability grants access for wildcard match', () => {
const hasAccess = hasCapability(userCapabilities, 'ai_resource:groq', 'use');
expect(hasAccess).toBe(true);
});
test('hasCapability denies access for unauthorized resource', () => {
const hasAccess = hasCapability(userCapabilities, 'tenant:other:*', 'read');
expect(hasAccess).toBe(false);
});
test('hasCapability denies access for unauthorized action', () => {
const hasAccess = hasCapability(userCapabilities, 'tenant:acme:*', 'admin');
expect(hasAccess).toBe(false);
});
test('hasCapability respects time constraints', () => {
const expiredCapabilities: Capability[] = [
{
resource: 'tenant:test:*',
actions: ['read'],
constraints: {
valid_until: new Date(Date.now() - 3600000).toISOString() // 1 hour ago
}
}
];
const hasAccess = hasCapability(expiredCapabilities, 'tenant:test:*', 'read');
expect(hasAccess).toBe(false);
});
});
describe('Password Functions', () => {
const testPassword = 'TestPassword123!';
test('hashPassword creates valid hash', async () => {
const hash = await hashPassword(testPassword);
expect(typeof hash).toBe('string');
expect(hash).not.toBe(testPassword);
expect(hash.startsWith('$2b$')).toBe(true); // bcrypt hash format
});
test('verifyPassword validates correct password', async () => {
const hash = await hashPassword(testPassword);
const isValid = await verifyPassword(testPassword, hash);
expect(isValid).toBe(true);
});
test('verifyPassword rejects incorrect password', async () => {
const hash = await hashPassword(testPassword);
const isValid = await verifyPassword('WrongPassword', hash);
expect(isValid).toBe(false);
});
test('different passwords create different hashes', async () => {
const hash1 = await hashPassword('Password1');
const hash2 = await hashPassword('Password2');
expect(hash1).not.toBe(hash2);
});
});
describe('Utility Functions', () => {
test('generateSecureToken creates token of correct length', () => {
const token = generateSecureToken(16);
expect(typeof token).toBe('string');
expect(token.length).toBe(32); // Hex encoding doubles the length
});
test('generateSecureToken creates different tokens', () => {
const token1 = generateSecureToken();
const token2 = generateSecureToken();
expect(token1).not.toBe(token2);
});
test('extractBearerToken extracts token correctly', () => {
const token = extractBearerToken('Bearer abc123token');
expect(token).toBe('abc123token');
});
test('extractBearerToken returns null for invalid format', () => {
expect(extractBearerToken('Invalid format')).toBeNull();
expect(extractBearerToken('Bearer')).toBeNull();
expect(extractBearerToken('')).toBeNull();
expect(extractBearerToken(undefined)).toBeNull();
});
});
describe('Capability Template Functions', () => {
test('createTenantCapabilities for admin user', () => {
const capabilities = createTenantCapabilities('acme', 'tenant_admin');
expect(capabilities).toHaveLength(2);
expect(capabilities[0].resource).toBe('tenant:acme:*');
expect(capabilities[0].actions).toContain('admin');
expect(capabilities[1].resource).toBe('ai_resource:*');
});
test('createTenantCapabilities for regular user', () => {
const capabilities = createTenantCapabilities('acme', 'tenant_user');
expect(capabilities).toHaveLength(3);
expect(capabilities[0].resource).toBe('tenant:acme:conversations');
expect(capabilities[0].actions).not.toContain('admin');
expect(capabilities[1].resource).toBe('tenant:acme:documents');
});
test('createSuperAdminCapabilities grants full access', () => {
const capabilities = createSuperAdminCapabilities();
expect(capabilities).toHaveLength(1);
expect(capabilities[0].resource).toBe('*');
expect(capabilities[0].actions).toEqual(['*']);
});
});
});

View File

@@ -0,0 +1,271 @@
/**
* Unit tests for cryptographic utilities
*/
import {
generateEncryptionKey,
encrypt,
decrypt,
sha256Hash,
generateHMAC,
verifyHMAC,
deriveTenantKey,
encryptForDatabase,
decryptFromDatabase,
generateSecurePassword
} from '../crypto';
describe('Cryptographic Utilities', () => {
describe('Key Generation', () => {
test('generateEncryptionKey creates valid key', () => {
const key = generateEncryptionKey();
expect(typeof key).toBe('string');
expect(key.length).toBe(64); // 32 bytes * 2 for hex encoding
expect(/^[a-f0-9]+$/i.test(key)).toBe(true); // Valid hex string
});
test('generateEncryptionKey creates different keys', () => {
const key1 = generateEncryptionKey();
const key2 = generateEncryptionKey();
expect(key1).not.toBe(key2);
});
});
describe('Encryption/Decryption', () => {
const testData = 'This is test data to encrypt';
const testKey = 'a'.repeat(64); // 32-byte key in hex
test('encrypt returns encrypted data with IV and tag', () => {
const result = encrypt(testData, testKey);
expect(result).toHaveProperty('encrypted');
expect(result).toHaveProperty('iv');
expect(result).toHaveProperty('tag');
expect(typeof result.encrypted).toBe('string');
expect(typeof result.iv).toBe('string');
expect(typeof result.tag).toBe('string');
expect(result.encrypted).not.toBe(testData);
});
test('decrypt successfully recovers original data', () => {
const { encrypted, iv, tag } = encrypt(testData, testKey);
const decrypted = decrypt(encrypted, testKey, iv, tag);
expect(decrypted).toBe(testData);
});
test('decrypt fails with wrong key', () => {
const { encrypted, iv, tag } = encrypt(testData, testKey);
const wrongKey = 'b'.repeat(64);
expect(() => {
decrypt(encrypted, wrongKey, iv, tag);
}).toThrow();
});
test('decrypt fails with tampered data', () => {
const { encrypted, iv, tag } = encrypt(testData, testKey);
const tamperedData = encrypted.slice(0, -2) + 'XX';
expect(() => {
decrypt(tamperedData, testKey, iv, tag);
}).toThrow();
});
test('encryption produces different results for same data', () => {
const result1 = encrypt(testData, testKey);
const result2 = encrypt(testData, testKey);
// Different IVs should produce different encrypted data
expect(result1.encrypted).not.toBe(result2.encrypted);
expect(result1.iv).not.toBe(result2.iv);
// But both should decrypt to same original data
const decrypted1 = decrypt(result1.encrypted, testKey, result1.iv, result1.tag);
const decrypted2 = decrypt(result2.encrypted, testKey, result2.iv, result2.tag);
expect(decrypted1).toBe(testData);
expect(decrypted2).toBe(testData);
});
});
describe('Hashing', () => {
test('sha256Hash creates consistent hash', () => {
const data = 'test data';
const hash1 = sha256Hash(data);
const hash2 = sha256Hash(data);
expect(hash1).toBe(hash2);
expect(typeof hash1).toBe('string');
expect(hash1.length).toBe(64); // SHA-256 produces 32 bytes = 64 hex chars
});
test('sha256Hash creates different hashes for different data', () => {
const hash1 = sha256Hash('data 1');
const hash2 = sha256Hash('data 2');
expect(hash1).not.toBe(hash2);
});
});
describe('HMAC', () => {
const testData = 'test data';
const testSecret = 'test secret';
test('generateHMAC creates valid signature', () => {
const signature = generateHMAC(testData, testSecret);
expect(typeof signature).toBe('string');
expect(signature.length).toBe(64); // HMAC-SHA256 = 64 hex chars
expect(/^[a-f0-9]+$/i.test(signature)).toBe(true);
});
test('verifyHMAC validates correct signature', () => {
const signature = generateHMAC(testData, testSecret);
const isValid = verifyHMAC(testData, signature, testSecret);
expect(isValid).toBe(true);
});
test('verifyHMAC rejects incorrect signature', () => {
const signature = generateHMAC(testData, testSecret);
const isValid = verifyHMAC(testData, signature + 'tampered', testSecret);
expect(isValid).toBe(false);
});
test('verifyHMAC rejects signature with wrong secret', () => {
const signature = generateHMAC(testData, testSecret);
const isValid = verifyHMAC(testData, signature, 'wrong secret');
expect(isValid).toBe(false);
});
test('HMAC is consistent for same inputs', () => {
const signature1 = generateHMAC(testData, testSecret);
const signature2 = generateHMAC(testData, testSecret);
expect(signature1).toBe(signature2);
});
});
describe('Key Derivation', () => {
const masterKey = 'a'.repeat(64); // 32-byte master key
const tenantId = 'tenant-123';
test('deriveTenantKey creates consistent key for tenant', () => {
const key1 = deriveTenantKey(masterKey, tenantId);
const key2 = deriveTenantKey(masterKey, tenantId);
expect(key1).toBe(key2);
expect(typeof key1).toBe('string');
expect(key1.length).toBe(64); // 32 bytes in hex
});
test('deriveTenantKey creates different keys for different tenants', () => {
const key1 = deriveTenantKey(masterKey, 'tenant-1');
const key2 = deriveTenantKey(masterKey, 'tenant-2');
expect(key1).not.toBe(key2);
});
test('deriveTenantKey creates different keys for different master keys', () => {
const masterKey2 = 'b'.repeat(64);
const key1 = deriveTenantKey(masterKey, tenantId);
const key2 = deriveTenantKey(masterKey2, tenantId);
expect(key1).not.toBe(key2);
});
});
describe('Database Encryption', () => {
const testData = { id: 1, name: 'test', data: [1, 2, 3] };
const testKey = 'a'.repeat(64);
test('encryptForDatabase encrypts JSON data', () => {
const encrypted = encryptForDatabase(testData, testKey);
expect(typeof encrypted).toBe('string');
expect(encrypted.split(':')).toHaveLength(3); // iv:tag:encrypted format
expect(encrypted).not.toContain('test'); // Should not contain original data
});
test('decryptFromDatabase recovers original JSON data', () => {
const encrypted = encryptForDatabase(testData, testKey);
const decrypted = decryptFromDatabase(encrypted, testKey);
expect(decrypted).toEqual(testData);
});
test('decryptFromDatabase fails with wrong key', () => {
const encrypted = encryptForDatabase(testData, testKey);
const wrongKey = 'b'.repeat(64);
expect(() => {
decryptFromDatabase(encrypted, wrongKey);
}).toThrow();
});
test('decryptFromDatabase fails with invalid format', () => {
expect(() => {
decryptFromDatabase('invalid-format', testKey);
}).toThrow('Invalid encrypted data format');
});
test('database encryption handles complex objects', () => {
const complexData = {
user: { id: 1, name: 'John Doe' },
preferences: { theme: 'dark', lang: 'en' },
timestamps: { created: new Date().toISOString() },
numbers: [1, 2.5, -3],
boolean: true,
null_value: null
};
const encrypted = encryptForDatabase(complexData, testKey);
const decrypted = decryptFromDatabase(encrypted, testKey);
expect(decrypted).toEqual(complexData);
});
});
describe('Password Generation', () => {
test('generateSecurePassword creates password of correct length', () => {
const password = generateSecurePassword(16);
expect(typeof password).toBe('string');
expect(password.length).toBe(16);
});
test('generateSecurePassword uses default length', () => {
const password = generateSecurePassword();
expect(password.length).toBe(16); // Default length
});
test('generateSecurePassword creates different passwords', () => {
const password1 = generateSecurePassword();
const password2 = generateSecurePassword();
expect(password1).not.toBe(password2);
});
test('generateSecurePassword includes variety of characters', () => {
const password = generateSecurePassword(50); // Longer for better test
expect(/[a-z]/.test(password)).toBe(true); // Lowercase
expect(/[A-Z]/.test(password)).toBe(true); // Uppercase
expect(/[0-9]/.test(password)).toBe(true); // Numbers
expect(/[!@#$%^&*]/.test(password)).toBe(true); // Special chars
});
test('generateSecurePassword creates strong passwords', () => {
// Test multiple passwords to ensure consistency
for (let i = 0; i < 10; i++) {
const password = generateSecurePassword(12);
expect(password.length).toBe(12);
expect(/[a-zA-Z0-9!@#$%^&*]/.test(password)).toBe(true);
}
});
});
});

View File

@@ -0,0 +1,22 @@
/**
* Test setup for utility functions
*/
// Mock environment variables for testing
process.env.JWT_SECRET = 'test-jwt-secret-for-testing-only';
process.env.MASTER_ENCRYPTION_KEY = 'test-master-key-32-bytes-long-test';
// Mock crypto for consistent testing
jest.mock('crypto', () => {
const originalCrypto = jest.requireActual('crypto');
return {
...originalCrypto,
randomBytes: jest.fn().mockImplementation((size: number) => {
return Buffer.alloc(size, 'a'); // Return consistent fake random bytes
}),
randomInt: jest.fn().mockReturnValue(5), // Return consistent fake random int
};
});
// Global test timeout
jest.setTimeout(10000);

View File

@@ -0,0 +1,329 @@
/**
* Unit tests for validation utilities
*/
import {
isValidEmail,
isValidDomain,
isValidPassword,
validateTenantCreateRequest,
validateChatRequest,
validateDocumentUpload,
sanitizeString,
isValidUUID,
validatePagination
} from '../validation';
describe('Validation Utilities', () => {
describe('Email Validation', () => {
test('validates correct email formats', () => {
const validEmails = [
'test@example.com',
'user.name@domain.co.uk',
'user+tag@example.org',
'user123@sub.domain.com'
];
validEmails.forEach(email => {
expect(isValidEmail(email)).toBe(true);
});
});
test('rejects invalid email formats', () => {
const invalidEmails = [
'invalid-email',
'@domain.com',
'user@',
'user@domain',
'user space@domain.com',
'',
'user@@domain.com'
];
invalidEmails.forEach(email => {
expect(isValidEmail(email)).toBe(false);
});
});
});
describe('Domain Validation', () => {
test('validates correct domain formats', () => {
const validDomains = [
'acme',
'test',
'company123',
'a1b2c3',
'long-domain-name-with-dashes'
];
validDomains.forEach(domain => {
expect(isValidDomain(domain)).toBe(true);
});
});
test('rejects invalid domain formats', () => {
const invalidDomains = [
'AB', // Too short
'a', // Too short
'domain-', // Ends with dash
'-domain', // Starts with dash
'domain.com', // Contains dot
'domain_name', // Contains underscore
'UPPERCASE', // Contains uppercase
'domain with spaces', // Contains spaces
'a'.repeat(51), // Too long
'' // Empty
];
invalidDomains.forEach(domain => {
expect(isValidDomain(domain)).toBe(false);
});
});
});
describe('Password Validation', () => {
test('validates strong passwords', () => {
const strongPasswords = [
'StrongPass123!',
'MySecure#Password1',
'Complex$Password99'
];
strongPasswords.forEach(password => {
const result = isValidPassword(password);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
test('rejects weak passwords with specific errors', () => {
const weakPasswords = [
{ password: 'short', expectedErrors: 5 }, // All criteria failed
{ password: 'toolongbutnothing', expectedErrors: 4 }, // No upper, digit, special
{ password: 'NoNumbers!', expectedErrors: 1 }, // No numbers
{ password: 'nonumbers123', expectedErrors: 2 }, // No upper, special
{ password: 'NOLOWER123!', expectedErrors: 1 }, // No lower
];
weakPasswords.forEach(({ password, expectedErrors }) => {
const result = isValidPassword(password);
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThanOrEqual(1);
});
});
});
describe('Tenant Create Request Validation', () => {
const validTenantRequest = {
name: 'Test Company',
domain: 'test',
template: 'basic',
max_users: 50,
resource_limits: {
cpu: '1000m',
memory: '2Gi',
storage: '10Gi'
}
};
test('validates correct tenant request', () => {
const result = validateTenantCreateRequest(validTenantRequest);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('rejects request with missing name', () => {
const request = { ...validTenantRequest, name: '' };
const result = validateTenantCreateRequest(request);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Tenant name is required');
});
test('rejects request with invalid domain', () => {
const request = { ...validTenantRequest, domain: 'invalid_domain' };
const result = validateTenantCreateRequest(request);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('Domain must be');
});
test('rejects request with invalid template', () => {
const request = { ...validTenantRequest, template: 'invalid' };
const result = validateTenantCreateRequest(request);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('Template must be one of');
});
test('rejects request with invalid max_users', () => {
const request = { ...validTenantRequest, max_users: -1 };
const result = validateTenantCreateRequest(request);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('Max users must be between');
});
test('validates resource limits format', () => {
const invalidRequests = [
{ ...validTenantRequest, resource_limits: { cpu: 'invalid' } },
{ ...validTenantRequest, resource_limits: { memory: '2Tb' } }, // Invalid unit
{ ...validTenantRequest, resource_limits: { storage: '10' } } // Missing unit
];
invalidRequests.forEach(request => {
const result = validateTenantCreateRequest(request);
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
});
});
describe('Chat Request Validation', () => {
const validChatRequest = {
message: 'Hello, how can I help you?',
conversation_id: 1,
model_id: 'gpt-4',
system_prompt: 'You are a helpful agent.',
context_sources: ['doc1', 'doc2']
};
test('validates correct chat request', () => {
const result = validateChatRequest(validChatRequest);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('rejects request with empty message', () => {
const request = { ...validChatRequest, message: '' };
const result = validateChatRequest(request);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Message is required');
});
test('rejects request with too long message', () => {
const request = { ...validChatRequest, message: 'a'.repeat(10001) };
const result = validateChatRequest(request);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('10000 characters or less');
});
test('rejects request with invalid conversation_id', () => {
const request = { ...validChatRequest, conversation_id: 0 };
const result = validateChatRequest(request);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Invalid conversation ID');
});
test('rejects request with too long system_prompt', () => {
const request = { ...validChatRequest, system_prompt: 'a'.repeat(2001) };
const result = validateChatRequest(request);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('2000 characters or less');
});
});
describe('Document Upload Validation', () => {
const createMockFile = (size: number, type: string, name: string) => ({
file: Buffer.alloc(size),
filename: name,
file_type: type
});
test('validates correct document upload', () => {
const upload = createMockFile(1000, 'text/plain', 'test.txt');
const result = validateDocumentUpload(upload);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
test('rejects upload with empty filename', () => {
const upload = createMockFile(1000, 'text/plain', '');
const result = validateDocumentUpload(upload);
expect(result.valid).toBe(false);
expect(result.errors).toContain('Filename is required');
});
test('rejects upload with unsupported file type', () => {
const upload = createMockFile(1000, 'image/jpeg', 'image.jpg');
const result = validateDocumentUpload(upload);
expect(result.valid).toBe(false);
expect(result.errors[0]).toContain('not supported');
});
test('rejects upload with file too large', () => {
const upload = createMockFile(51 * 1024 * 1024, 'text/plain', 'large.txt');
const result = validateDocumentUpload(upload);
expect(result.valid).toBe(false);
expect(result.errors).toContain('File size must be 50MB or less');
});
test('validates supported file types', () => {
const supportedTypes = [
'text/plain',
'text/markdown',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/csv'
];
supportedTypes.forEach(type => {
const upload = createMockFile(1000, type, 'test.file');
const result = validateDocumentUpload(upload);
expect(result.valid).toBe(true);
});
});
});
describe('Utility Validations', () => {
test('sanitizeString removes dangerous content', () => {
const dangerous = '<script>alert("xss")</script><p onclick="alert()">Click me</p>';
const sanitized = sanitizeString(dangerous);
expect(sanitized).not.toContain('<script>');
expect(sanitized).not.toContain('onclick');
expect(sanitized).not.toContain('javascript:');
});
test('isValidUUID validates correct UUIDs', () => {
const validUUIDs = [
'123e4567-e89b-12d3-a456-426614174000',
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
'6ba7b810-9dad-11d1-80b4-00c04fd430c8'
];
validUUIDs.forEach(uuid => {
expect(isValidUUID(uuid)).toBe(true);
});
});
test('isValidUUID rejects invalid UUIDs', () => {
const invalidUUIDs = [
'not-a-uuid',
'123e4567-e89b-12d3-a456', // Too short
'123e4567-e89b-12d3-a456-426614174000-extra', // Too long
'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', // Invalid characters
''
];
invalidUUIDs.forEach(uuid => {
expect(isValidUUID(uuid)).toBe(false);
});
});
test('validatePagination normalizes and validates parameters', () => {
// Test valid parameters
const result1 = validatePagination(2, 50);
expect(result1.page).toBe(2);
expect(result1.limit).toBe(50);
expect(result1.errors).toHaveLength(0);
// Test defaults
const result2 = validatePagination();
expect(result2.page).toBe(1);
expect(result2.limit).toBe(20);
// Test invalid parameters
const result3 = validatePagination(-1, 150);
expect(result3.page).toBe(1); // Corrected
expect(result3.limit).toBe(100); // Corrected to max
expect(result3.errors.length).toBeGreaterThan(0);
});
});
});

216
packages/utils/src/auth.ts Normal file
View File

@@ -0,0 +1,216 @@
// Authentication and Authorization Utilities
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { JWTPayload, Capability } from '@gt2/types';
// JWT Configuration
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
/**
* Generate a cryptographic hash for capability verification
*/
export function generateCapabilityHash(capabilities: Capability[]): string {
const capabilityString = JSON.stringify(capabilities, Object.keys(capabilities).sort());
return crypto.createHmac('sha256', JWT_SECRET).update(capabilityString).digest('hex');
}
/**
* Verify capability hash to ensure JWT hasn't been tampered with
*/
export function verifyCapabilityHash(capabilities: Capability[], hash: string): boolean {
const expectedHash = generateCapabilityHash(capabilities);
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
}
/**
* Create a capability-based JWT token
*/
export function createJWT(payload: Omit<JWTPayload, 'capability_hash' | 'exp' | 'iat'>): string {
const capability_hash = generateCapabilityHash(payload.capabilities);
const fullPayload: JWTPayload = {
...payload,
capability_hash,
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // 24 hours
iat: Math.floor(Date.now() / 1000)
};
return jwt.sign(fullPayload, JWT_SECRET, { algorithm: 'HS256' });
}
/**
* Verify and decode a JWT token
*/
export function verifyJWT(token: string): JWTPayload | null {
try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
// Verify capability hash to ensure token hasn't been tampered with
if (!verifyCapabilityHash(decoded.capabilities, decoded.capability_hash)) {
throw new Error('Invalid capability hash');
}
return decoded;
} catch (error) {
return null;
}
}
/**
* Check if user has required capability
*
* Supports wildcard matching for resources:
* - "*" matches all resources
* - "documents/*" matches all resources starting with "documents/"
* - "documents/read" matches only exact resource "documents/read"
*/
export function hasCapability(
userCapabilities: Capability[],
resource: string,
action: string
): boolean {
return userCapabilities.some(cap => {
// Check if capability matches resource (support wildcards)
let resourceMatch = false;
if (cap.resource === '*') {
// Wildcard matches everything
resourceMatch = true;
} else if (cap.resource === resource) {
// Exact match
resourceMatch = true;
} else if (cap.resource.endsWith('/*')) {
// Prefix wildcard: "documents/*" matches "documents/read", "documents/write", etc.
const prefix = cap.resource.slice(0, -1); // Remove trailing "*", keep "/"
resourceMatch = resource.startsWith(prefix);
} else if (cap.resource.endsWith('*')) {
// Trailing wildcard: "documents*" matches "documents", "documents/read", etc.
const prefix = cap.resource.slice(0, -1); // Remove trailing "*"
resourceMatch = resource.startsWith(prefix);
}
// Check if capability includes required action
const actionMatch = cap.actions.includes('*') || cap.actions.includes(action);
// Check constraints if present
if (cap.constraints) {
// Check validity period
if (cap.constraints.valid_until) {
const validUntil = new Date(cap.constraints.valid_until);
if (new Date() > validUntil) {
return false;
}
}
// Additional constraint checks can be added here
}
return resourceMatch && actionMatch;
});
}
/**
* Hash password for storage
*/
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(12);
return bcrypt.hash(password, salt);
}
/**
* Verify password against hash
*/
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
/**
* Generate secure random token
*/
export function generateSecureToken(length: number = 32): string {
return crypto.randomBytes(length).toString('hex');
}
/**
* Create tenant-scoped capabilities
*/
export function createTenantCapabilities(
tenantDomain: string,
userType: 'tenant_admin' | 'tenant_user'
): Capability[] {
const baseResource = `tenant:${tenantDomain}`;
if (userType === 'tenant_admin') {
return [
{
resource: `${baseResource}:*`,
actions: ['read', 'write', 'admin'],
constraints: {}
},
{
resource: 'ai_resource:*',
actions: ['use'],
constraints: {
usage_limits: {
max_requests_per_hour: 1000,
max_tokens_per_request: 4000
}
}
}
];
} else {
return [
{
resource: `${baseResource}:conversations`,
actions: ['read', 'write'],
constraints: {}
},
{
resource: `${baseResource}:documents`,
actions: ['read', 'write'],
constraints: {}
},
{
resource: 'ai_resource:*',
actions: ['use'],
constraints: {
usage_limits: {
max_requests_per_hour: 100,
max_tokens_per_request: 4000
}
}
}
];
}
}
/**
* Create super admin capabilities
*/
export function createSuperAdminCapabilities(): Capability[] {
return [
{
resource: '*',
actions: ['*'],
constraints: {}
}
];
}
/**
* Extract Bearer token from Authorization header
*/
export function extractBearerToken(authHeader?: string): string | null {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}
return authHeader.substring(7);
}
/**
* Check if JWT token is expired
*/
export function isTokenExpired(token: JWTPayload): boolean {
return Date.now() >= token.exp * 1000;
}

View File

@@ -0,0 +1,147 @@
// Cryptographic utilities for GT 2.0
import crypto from 'crypto';
// Encryption configuration
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32; // 256 bits
const IV_LENGTH = 16; // 128 bits
const TAG_LENGTH = 16; // 128 bits
/**
* Generate a random encryption key
*/
export function generateEncryptionKey(): string {
return crypto.randomBytes(KEY_LENGTH).toString('hex');
}
/**
* Encrypt data using AES-256-GCM
*/
export function encrypt(data: string, keyHex: string): {
encrypted: string;
iv: string;
tag: string;
} {
const key = Buffer.from(keyHex, 'hex');
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipher(ALGORITHM, key);
cipher.setAAD(Buffer.from('GT2-TENANT-DATA'));
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex')
};
}
/**
* Decrypt data using AES-256-GCM
*/
export function decrypt(
encryptedData: string,
keyHex: string,
ivHex: string,
tagHex: string
): string {
const key = Buffer.from(keyHex, 'hex');
const iv = Buffer.from(ivHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
const decipher = crypto.createDecipher(ALGORITHM, key);
decipher.setAuthTag(tag);
decipher.setAAD(Buffer.from('GT2-TENANT-DATA'));
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Hash data using SHA-256
*/
export function sha256Hash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Generate HMAC signature
*/
export function generateHMAC(data: string, secret: string): string {
return crypto.createHmac('sha256', secret).update(data).digest('hex');
}
/**
* Verify HMAC signature
*/
export function verifyHMAC(data: string, signature: string, secret: string): boolean {
const expectedSignature = generateHMAC(data, secret);
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
/**
* Generate tenant-specific encryption key from master key and tenant ID
*/
export function deriveTenantKey(masterKey: string, tenantId: string): string {
const key = crypto.pbkdf2Sync(
tenantId,
Buffer.from(masterKey, 'hex'),
100000, // iterations
KEY_LENGTH,
'sha256'
);
return key.toString('hex');
}
/**
* Encrypt JSON data for database storage
*/
export function encryptForDatabase(
data: any,
encryptionKey: string
): string {
const jsonString = JSON.stringify(data);
const { encrypted, iv, tag } = encrypt(jsonString, encryptionKey);
// Combine all components into a single string
return `${iv}:${tag}:${encrypted}`;
}
/**
* Decrypt JSON data from database storage
*/
export function decryptFromDatabase(
encryptedData: string,
encryptionKey: string
): any {
const [iv, tag, encrypted] = encryptedData.split(':');
if (!iv || !tag || !encrypted) {
throw new Error('Invalid encrypted data format');
}
const jsonString = decrypt(encrypted, encryptionKey, iv, tag);
return JSON.parse(jsonString);
}
/**
* Generate a secure random password
*/
export function generateSecurePassword(length: number = 16): string {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
const randomIndex = crypto.randomInt(0, charset.length);
password += charset[randomIndex];
}
return password;
}

View File

@@ -0,0 +1,211 @@
// Database utility functions
import path from 'path';
import crypto from 'crypto';
/**
* Generate SQLite database path for tenant
*/
export function getTenantDatabasePath(tenantDomain: string, dataDir: string = '/data'): string {
return path.join(dataDir, tenantDomain, 'app.db');
}
/**
* Generate ChromaDB collection name for tenant
*/
export function getTenantChromaCollection(tenantDomain: string): string {
// ChromaDB collection names must be alphanumeric with underscores
return `gt2_${tenantDomain.replace(/-/g, '_')}_documents`;
}
/**
* Generate Redis key prefix for tenant
*/
export function getTenantRedisPrefix(tenantDomain: string): string {
return `gt2:${tenantDomain}:`;
}
/**
* Generate MinIO bucket name for tenant
*/
export function getTenantMinioBucket(tenantDomain: string): string {
// MinIO bucket names must be lowercase and DNS-compliant
return `gt2-${tenantDomain}-files`;
}
/**
* Generate SQLite WAL mode configuration
*/
export function getSQLiteWALConfig(): string {
return `
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=1000;
PRAGMA foreign_keys=ON;
PRAGMA temp_store=MEMORY;
`;
}
/**
* Generate SQLite encryption configuration
*/
export function getSQLiteEncryptionConfig(encryptionKey: string): string {
return `PRAGMA key='${encryptionKey}';`;
}
/**
* Create tenant database schema (SQLite)
*/
export function getTenantDatabaseSchema(): string {
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', 'agent', '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
);
-- Zero Downtime Guardian: SQLite does not support CONCURRENTLY
-- Performance optimizations deferred to post-deployment for Guardian compliance
-- TODO: Add database optimizations after deployment verification
-- 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
*/
export function generateDocumentChunkId(documentId: number, chunkIndex: number): string {
const data = `${documentId}-${chunkIndex}-${Date.now()}`;
return crypto.createHash('sha256').update(data).digest('hex').substring(0, 32);
}
/**
* Parse connection string for database configuration
*/
export function parseConnectionString(connectionString: string): {
host?: string;
port?: number;
database?: string;
username?: string;
password?: string;
options?: Record<string, string>;
} {
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.)
*/
export function escapeSQLIdentifier(identifier: string): string {
return `"${identifier.replace(/"/g, '""')}"`;
}
/**
* Generate database backup filename
*/
export function generateBackupFilename(tenantDomain: string, timestamp?: Date): string {
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`;
}

View File

@@ -0,0 +1,7 @@
// GT 2.0 Shared Utility Functions
export * from './auth';
export * from './crypto';
export * from './validation';
export * from './database';
export * from './tenant';

View File

@@ -0,0 +1,354 @@
// Tenant management utilities
import { Tenant, TenantCreateRequest } from '@gt2/types';
import { generateEncryptionKey, deriveTenantKey } from './crypto';
/**
* Generate Kubernetes namespace name for tenant
*/
export function generateTenantNamespace(domain: string): string {
return `gt-${domain}`;
}
/**
* Generate tenant subdomain
*/
export function generateTenantSubdomain(domain: string): string {
return domain; // For now, subdomain matches domain
}
/**
* Generate OS user ID for tenant isolation
*/
export function generateTenantUserId(tenantId: number): number {
const baseUserId = 10000; // Start user IDs from 10000
return baseUserId + tenantId;
}
/**
* Generate OS group ID for tenant isolation
*/
export function generateTenantGroupId(tenantId: number): number {
return generateTenantUserId(tenantId); // Use same ID for group
}
/**
* Get tenant data directory path
*/
export function getTenantDataPath(domain: string, baseDataDir: string = '/data'): string {
return `${baseDataDir}/${domain}`;
}
/**
* Get default resource limits based on template
*/
export function getTemplateResourceLimits(template: string): {
cpu: string;
memory: string;
storage: string;
} {
switch (template) {
case 'basic':
return {
cpu: '500m',
memory: '1Gi',
storage: '5Gi'
};
case 'professional':
return {
cpu: '1000m',
memory: '2Gi',
storage: '20Gi'
};
case 'enterprise':
return {
cpu: '2000m',
memory: '4Gi',
storage: '100Gi'
};
default:
return {
cpu: '500m',
memory: '1Gi',
storage: '5Gi'
};
}
}
/**
* Get default max users based on template
*/
export function getTemplateMaxUsers(template: string): number {
switch (template) {
case 'basic':
return 10;
case 'professional':
return 100;
case 'enterprise':
return 1000;
default:
return 10;
}
}
/**
* Validate tenant domain availability (placeholder - would check database in real implementation)
*/
export function isDomainAvailable(domain: string): boolean {
// In real implementation, this would check the database
// For now, just check format
const reservedDomains = ['admin', 'api', 'www', 'mail', 'ftp', 'localhost', 'gt2'];
return !reservedDomains.includes(domain.toLowerCase());
}
/**
* Generate complete tenant configuration from create request
*/
export function generateTenantConfig(
request: TenantCreateRequest,
masterEncryptionKey: string
): Partial<Tenant> {
const template = request.template || 'basic';
const resourceLimits = request.resource_limits || getTemplateResourceLimits(template);
const maxUsers = request.max_users || getTemplateMaxUsers(template);
return {
name: request.name.trim(),
domain: request.domain.toLowerCase(),
template,
max_users: maxUsers,
resource_limits: resourceLimits,
namespace: generateTenantNamespace(request.domain),
subdomain: generateTenantSubdomain(request.domain),
status: 'pending'
};
}
/**
* Generate Kubernetes deployment YAML for tenant
*/
export function generateTenantDeploymentYAML(tenant: Tenant, tenantUserId: number): string {
return `
apiVersion: v1
kind: Namespace
metadata:
name: ${tenant.namespace}
labels:
gt.tenant: ${tenant.domain}
gt.template: ${tenant.template}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ${tenant.domain}-isolation
namespace: ${tenant.namespace}
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
name: gt-admin
egress:
- to:
- namespaceSelector:
matchLabels:
name: gt-resource
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ${tenant.domain}-config
namespace: ${tenant.namespace}
data:
TENANT_ID: "${tenant.id}"
TENANT_DOMAIN: "${tenant.domain}"
TENANT_NAME: "${tenant.name}"
DATABASE_PATH: "/data/${tenant.domain}/app.db"
CHROMA_COLLECTION: "gt2_${tenant.domain.replace(/-/g, '_')}_documents"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${tenant.domain}-app
namespace: ${tenant.namespace}
labels:
app: ${tenant.domain}-app
tenant: ${tenant.domain}
spec:
replicas: 1
selector:
matchLabels:
app: ${tenant.domain}-app
template:
metadata:
labels:
app: ${tenant.domain}-app
tenant: ${tenant.domain}
spec:
securityContext:
runAsUser: ${tenantUserId}
runAsGroup: ${tenantUserId}
fsGroup: ${tenantUserId}
containers:
- name: frontend
image: gt2/tenant-frontend:latest
ports:
- containerPort: 3000
name: frontend
env:
- name: NEXT_PUBLIC_API_URL
value: "http://localhost:8000"
- name: NEXT_PUBLIC_WS_URL
value: "ws://localhost:8000"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "${tenant.resource_limits.cpu}"
memory: "${tenant.resource_limits.memory}"
volumeMounts:
- name: tenant-data
mountPath: /data/${tenant.domain}
- name: backend
image: gt2/tenant-backend:latest
ports:
- containerPort: 8000
name: backend
envFrom:
- configMapRef:
name: ${tenant.domain}-config
env:
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: ${tenant.domain}-secrets
key: encryption-key
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "${tenant.resource_limits.cpu}"
memory: "${tenant.resource_limits.memory}"
volumeMounts:
- name: tenant-data
mountPath: /data/${tenant.domain}
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: tenant-data
persistentVolumeClaim:
claimName: ${tenant.domain}-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ${tenant.domain}-data
namespace: ${tenant.namespace}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: ${tenant.resource_limits.storage}
---
apiVersion: v1
kind: Secret
metadata:
name: ${tenant.domain}-secrets
namespace: ${tenant.namespace}
type: Opaque
data:
encryption-key: ${Buffer.from(tenant.encryption_key || '').toString('base64')}
---
apiVersion: v1
kind: Service
metadata:
name: ${tenant.domain}-service
namespace: ${tenant.namespace}
spec:
selector:
app: ${tenant.domain}-app
ports:
- name: frontend
port: 3000
targetPort: 3000
- name: backend
port: 8000
targetPort: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ${tenant.domain}-ingress
namespace: ${tenant.namespace}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: ${tenant.subdomain}.gt2.local
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: ${tenant.domain}-service
port:
number: 8000
- path: /
pathType: Prefix
backend:
service:
name: ${tenant.domain}-service
port:
number: 3000
`.trim();
}
/**
* Calculate tenant usage costs
*/
export function calculateTenantCosts(
cpuUsage: number, // CPU hours
memoryUsage: number, // Memory GB-hours
storageUsage: number, // Storage GB-hours
aiTokens: number // AI tokens used
): {
cpu_cost_cents: number;
memory_cost_cents: number;
storage_cost_cents: number;
ai_cost_cents: number;
total_cost_cents: number;
} {
// Pricing (example rates)
const CPU_COST_PER_HOUR = 5; // 5 cents per CPU hour
const MEMORY_COST_PER_GB_HOUR = 1; // 1 cent per GB-hour
const STORAGE_COST_PER_GB_HOUR = 0.1; // 0.1 cents per GB-hour
const AI_COST_PER_1K_TOKENS = 0.5; // 0.5 cents per 1K tokens
const cpu_cost_cents = Math.round(cpuUsage * CPU_COST_PER_HOUR);
const memory_cost_cents = Math.round(memoryUsage * MEMORY_COST_PER_GB_HOUR);
const storage_cost_cents = Math.round(storageUsage * STORAGE_COST_PER_GB_HOUR);
const ai_cost_cents = Math.round((aiTokens / 1000) * AI_COST_PER_1K_TOKENS);
return {
cpu_cost_cents,
memory_cost_cents,
storage_cost_cents,
ai_cost_cents,
total_cost_cents: cpu_cost_cents + memory_cost_cents + storage_cost_cents + ai_cost_cents
};
}

View File

@@ -0,0 +1,276 @@
// Input validation utilities
import { TenantCreateRequest, ChatRequest, DocumentUploadRequest } from '@gt2/types';
/**
* Validate email format
* Uses a safer regex pattern that avoids potential ReDoS vulnerabilities
*/
export function isValidEmail(email: string): boolean {
// Safer regex pattern that avoids catastrophic backtracking
// Limits: max 64 chars local part, max 255 chars domain
if (!email || email.length > 320) return false;
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
return emailRegex.test(email);
}
/**
* Validate domain name format
*/
export function isValidDomain(domain: string): boolean {
// Must be lowercase alphanumeric with hyphens, 3-50 characters
const domainRegex = /^[a-z0-9][a-z0-9-]{1,48}[a-z0-9]$/;
return domainRegex.test(domain);
}
/**
* Validate password strength
*/
export function isValidPassword(password: string): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Password must contain at least one special character');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate tenant creation request
*/
export function validateTenantCreateRequest(request: TenantCreateRequest): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
// Validate name
if (!request.name || request.name.trim().length === 0) {
errors.push('Tenant name is required');
} else if (request.name.length > 100) {
errors.push('Tenant name must be 100 characters or less');
}
// Validate domain
if (!request.domain) {
errors.push('Domain is required');
} else if (!isValidDomain(request.domain)) {
errors.push('Domain must be 3-50 characters, lowercase alphanumeric with hyphens');
}
// Validate template
const validTemplates = ['basic', 'professional', 'enterprise'];
if (request.template && !validTemplates.includes(request.template)) {
errors.push(`Template must be one of: ${validTemplates.join(', ')}`);
}
// Validate max_users
if (request.max_users !== undefined) {
if (request.max_users < 1 || request.max_users > 10000) {
errors.push('Max users must be between 1 and 10000');
}
}
// Validate resource limits
if (request.resource_limits) {
if (request.resource_limits.cpu) {
if (!/^\d+m?$/.test(request.resource_limits.cpu)) {
errors.push('CPU limit must be in format like "1000m" or "2"');
}
}
if (request.resource_limits.memory) {
if (!/^\d+(Mi|Gi)$/.test(request.resource_limits.memory)) {
errors.push('Memory limit must be in format like "2Gi" or "512Mi"');
}
}
if (request.resource_limits.storage) {
if (!/^\d+(Mi|Gi|Ti)$/.test(request.resource_limits.storage)) {
errors.push('Storage limit must be in format like "10Gi" or "100Mi"');
}
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate chat request
*/
export function validateChatRequest(request: ChatRequest): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (!request.message || request.message.trim().length === 0) {
errors.push('Message is required');
} else if (request.message.length > 10000) {
errors.push('Message must be 10000 characters or less');
}
if (request.conversation_id !== undefined && request.conversation_id < 1) {
errors.push('Invalid conversation ID');
}
if (request.system_prompt && request.system_prompt.length > 2000) {
errors.push('System prompt must be 2000 characters or less');
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Validate file upload request
*/
export function validateDocumentUpload(request: DocumentUploadRequest): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (!request.filename || request.filename.trim().length === 0) {
errors.push('Filename is required');
} else if (request.filename.length > 255) {
errors.push('Filename must be 255 characters or less');
}
// Validate file type
const allowedTypes = [
'text/plain',
'text/markdown',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/csv'
];
if (!allowedTypes.includes(request.file_type)) {
errors.push(`File type ${request.file_type} is not supported`);
}
// Check file size (assuming file is Buffer with length property)
if (Buffer.isBuffer(request.file)) {
const maxSize = 50 * 1024 * 1024; // 50MB
if (request.file.length > maxSize) {
errors.push('File size must be 50MB or less');
}
} else if (request.file instanceof File) {
const maxSize = 50 * 1024 * 1024; // 50MB
if (request.file.size > maxSize) {
errors.push('File size must be 50MB or less');
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Sanitize string input to prevent injection attacks
*
* Uses a comprehensive approach to remove potentially dangerous content:
* - Removes ALL HTML tags (not just script)
* - Removes dangerous URL schemes
* - Handles various encoding bypass attempts
*
* For full HTML sanitization in user-facing contexts, consider using
* a dedicated library like DOMPurify on the client side.
*/
export function sanitizeString(input: string): string {
if (!input) return '';
let sanitized = input;
// Remove null bytes
sanitized = sanitized.replace(/\x00/g, '');
// Remove ALL HTML tags using a simpler, safer approach
// This is more secure than trying to match specific tags
// codeql[js/polynomial-redos] regex /<[^>]*>/g is linear, not vulnerable to ReDoS
// codeql[js/incomplete-multi-character-sanitization] stripping all tags is intentional defense-in-depth
sanitized = sanitized.replace(/<[^>]*>/g, '');
// Remove dangerous URL schemes (with various bypass attempts)
// Handles: javascript:, vbscript:, data:, etc.
const dangerousSchemes = /(?:java|vb|live)?script\s*:|data\s*:|vbscript\s*:/gi;
sanitized = sanitized.replace(dangerousSchemes, '');
// Remove event handlers with various patterns
// Matches: onclick, onerror, onload, etc. with = and value
// codeql[js/incomplete-multi-character-sanitization] used with HTML tag stripping above for defense-in-depth
sanitized = sanitized.replace(/on[a-z]+\s*=\s*(['"]?).*?\1/gi, '');
// Remove expression() - IE-specific CSS injection
sanitized = sanitized.replace(/expression\s*\(/gi, '');
return sanitized.trim();
}
/**
* Validate UUID format
*/
export function isValidUUID(uuid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(uuid);
}
/**
* Validate pagination parameters
*/
export function validatePagination(page?: number, limit?: number): {
page: number;
limit: number;
errors: string[];
} {
const errors: string[] = [];
let validatedPage = page || 1;
let validatedLimit = limit || 20;
if (validatedPage < 1) {
errors.push('Page must be 1 or greater');
validatedPage = 1;
}
if (validatedLimit < 1 || validatedLimit > 100) {
errors.push('Limit must be between 1 and 100');
validatedLimit = Math.min(Math.max(validatedLimit, 1), 100);
}
return {
page: validatedPage,
limit: validatedLimit,
errors
};
}