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:
1
packages/utils/dist/__tests__/auth.test.d.ts
vendored
Normal file
1
packages/utils/dist/__tests__/auth.test.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
212
packages/utils/dist/__tests__/auth.test.js
vendored
Normal file
212
packages/utils/dist/__tests__/auth.test.js
vendored
Normal file
@@ -0,0 +1,212 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* Unit tests for authentication utilities
|
||||
*/
|
||||
const auth_1 = require("../auth");
|
||||
describe('Authentication Utilities', () => {
|
||||
describe('Capability Hash Functions', () => {
|
||||
const testCapabilities = [
|
||||
{
|
||||
resource: 'tenant:test:*',
|
||||
actions: ['read', 'write'],
|
||||
constraints: {}
|
||||
}
|
||||
];
|
||||
test('generateCapabilityHash creates consistent hash', () => {
|
||||
const hash1 = (0, auth_1.generateCapabilityHash)(testCapabilities);
|
||||
const hash2 = (0, auth_1.generateCapabilityHash)(testCapabilities);
|
||||
expect(hash1).toBe(hash2);
|
||||
expect(typeof hash1).toBe('string');
|
||||
expect(hash1.length).toBeGreaterThan(0);
|
||||
});
|
||||
test('verifyCapabilityHash validates correct hash', () => {
|
||||
const hash = (0, auth_1.generateCapabilityHash)(testCapabilities);
|
||||
const isValid = (0, auth_1.verifyCapabilityHash)(testCapabilities, hash);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
test('verifyCapabilityHash rejects incorrect hash', () => {
|
||||
const isValid = (0, auth_1.verifyCapabilityHash)(testCapabilities, 'incorrect-hash');
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
test('capability hash changes with different capabilities', () => {
|
||||
const capabilities1 = [
|
||||
{ resource: 'tenant:test1:*', actions: ['read'], constraints: {} }
|
||||
];
|
||||
const capabilities2 = [
|
||||
{ resource: 'tenant:test2:*', actions: ['write'], constraints: {} }
|
||||
];
|
||||
const hash1 = (0, auth_1.generateCapabilityHash)(capabilities1);
|
||||
const hash2 = (0, auth_1.generateCapabilityHash)(capabilities2);
|
||||
expect(hash1).not.toBe(hash2);
|
||||
});
|
||||
});
|
||||
describe('JWT Functions', () => {
|
||||
const testPayload = {
|
||||
sub: 'test@example.com',
|
||||
tenant_id: '123',
|
||||
user_type: 'tenant_user',
|
||||
capabilities: [
|
||||
{
|
||||
resource: 'tenant:test:*',
|
||||
actions: ['read', 'write'],
|
||||
constraints: {}
|
||||
}
|
||||
]
|
||||
};
|
||||
test('createJWT generates valid token', () => {
|
||||
const token = (0, auth_1.createJWT)(testPayload);
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
||||
});
|
||||
test('verifyJWT validates correct token', () => {
|
||||
const token = (0, auth_1.createJWT)(testPayload);
|
||||
const decoded = (0, auth_1.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 = (0, auth_1.verifyJWT)('invalid.token.here');
|
||||
expect(decoded).toBeNull();
|
||||
});
|
||||
test('verifyJWT rejects tampered token', () => {
|
||||
const token = (0, auth_1.createJWT)(testPayload);
|
||||
const tamperedToken = token.slice(0, -10) + 'tampered123';
|
||||
const decoded = (0, auth_1.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((0, auth_1.isTokenExpired)(expiredPayload)).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((0, auth_1.isTokenExpired)(validPayload)).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('Capability Authorization', () => {
|
||||
const userCapabilities = [
|
||||
{
|
||||
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 = (0, auth_1.hasCapability)(userCapabilities, 'tenant:acme:conversations', 'read');
|
||||
expect(hasAccess).toBe(true);
|
||||
});
|
||||
test('hasCapability grants access for wildcard match', () => {
|
||||
const hasAccess = (0, auth_1.hasCapability)(userCapabilities, 'ai_resource:groq', 'use');
|
||||
expect(hasAccess).toBe(true);
|
||||
});
|
||||
test('hasCapability denies access for unauthorized resource', () => {
|
||||
const hasAccess = (0, auth_1.hasCapability)(userCapabilities, 'tenant:other:*', 'read');
|
||||
expect(hasAccess).toBe(false);
|
||||
});
|
||||
test('hasCapability denies access for unauthorized action', () => {
|
||||
const hasAccess = (0, auth_1.hasCapability)(userCapabilities, 'tenant:acme:*', 'admin');
|
||||
expect(hasAccess).toBe(false);
|
||||
});
|
||||
test('hasCapability respects time constraints', () => {
|
||||
const expiredCapabilities = [
|
||||
{
|
||||
resource: 'tenant:test:*',
|
||||
actions: ['read'],
|
||||
constraints: {
|
||||
valid_until: new Date(Date.now() - 3600000).toISOString() // 1 hour ago
|
||||
}
|
||||
}
|
||||
];
|
||||
const hasAccess = (0, auth_1.hasCapability)(expiredCapabilities, 'tenant:test:*', 'read');
|
||||
expect(hasAccess).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('Password Functions', () => {
|
||||
const testPassword = 'TestPassword123!';
|
||||
test('hashPassword creates valid hash', async () => {
|
||||
const hash = await (0, auth_1.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 (0, auth_1.hashPassword)(testPassword);
|
||||
const isValid = await (0, auth_1.verifyPassword)(testPassword, hash);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
test('verifyPassword rejects incorrect password', async () => {
|
||||
const hash = await (0, auth_1.hashPassword)(testPassword);
|
||||
const isValid = await (0, auth_1.verifyPassword)('WrongPassword', hash);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
test('different passwords create different hashes', async () => {
|
||||
const hash1 = await (0, auth_1.hashPassword)('Password1');
|
||||
const hash2 = await (0, auth_1.hashPassword)('Password2');
|
||||
expect(hash1).not.toBe(hash2);
|
||||
});
|
||||
});
|
||||
describe('Utility Functions', () => {
|
||||
test('generateSecureToken creates token of correct length', () => {
|
||||
const token = (0, auth_1.generateSecureToken)(16);
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.length).toBe(32); // Hex encoding doubles the length
|
||||
});
|
||||
test('generateSecureToken creates different tokens', () => {
|
||||
const token1 = (0, auth_1.generateSecureToken)();
|
||||
const token2 = (0, auth_1.generateSecureToken)();
|
||||
expect(token1).not.toBe(token2);
|
||||
});
|
||||
test('extractBearerToken extracts token correctly', () => {
|
||||
const token = (0, auth_1.extractBearerToken)('Bearer abc123token');
|
||||
expect(token).toBe('abc123token');
|
||||
});
|
||||
test('extractBearerToken returns null for invalid format', () => {
|
||||
expect((0, auth_1.extractBearerToken)('Invalid format')).toBeNull();
|
||||
expect((0, auth_1.extractBearerToken)('Bearer')).toBeNull();
|
||||
expect((0, auth_1.extractBearerToken)('')).toBeNull();
|
||||
expect((0, auth_1.extractBearerToken)(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('Capability Template Functions', () => {
|
||||
test('createTenantCapabilities for admin user', () => {
|
||||
const capabilities = (0, auth_1.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 = (0, auth_1.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 = (0, auth_1.createSuperAdminCapabilities)();
|
||||
expect(capabilities).toHaveLength(1);
|
||||
expect(capabilities[0].resource).toBe('*');
|
||||
expect(capabilities[0].actions).toEqual(['*']);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
packages/utils/dist/__tests__/crypto.test.d.ts
vendored
Normal file
1
packages/utils/dist/__tests__/crypto.test.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
204
packages/utils/dist/__tests__/crypto.test.js
vendored
Normal file
204
packages/utils/dist/__tests__/crypto.test.js
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* Unit tests for cryptographic utilities
|
||||
*/
|
||||
const crypto_1 = require("../crypto");
|
||||
describe('Cryptographic Utilities', () => {
|
||||
describe('Key Generation', () => {
|
||||
test('generateEncryptionKey creates valid key', () => {
|
||||
const key = (0, crypto_1.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 = (0, crypto_1.generateEncryptionKey)();
|
||||
const key2 = (0, crypto_1.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 = (0, crypto_1.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 } = (0, crypto_1.encrypt)(testData, testKey);
|
||||
const decrypted = (0, crypto_1.decrypt)(encrypted, testKey, iv, tag);
|
||||
expect(decrypted).toBe(testData);
|
||||
});
|
||||
test('decrypt fails with wrong key', () => {
|
||||
const { encrypted, iv, tag } = (0, crypto_1.encrypt)(testData, testKey);
|
||||
const wrongKey = 'b'.repeat(64);
|
||||
expect(() => {
|
||||
(0, crypto_1.decrypt)(encrypted, wrongKey, iv, tag);
|
||||
}).toThrow();
|
||||
});
|
||||
test('decrypt fails with tampered data', () => {
|
||||
const { encrypted, iv, tag } = (0, crypto_1.encrypt)(testData, testKey);
|
||||
const tamperedData = encrypted.slice(0, -2) + 'XX';
|
||||
expect(() => {
|
||||
(0, crypto_1.decrypt)(tamperedData, testKey, iv, tag);
|
||||
}).toThrow();
|
||||
});
|
||||
test('encryption produces different results for same data', () => {
|
||||
const result1 = (0, crypto_1.encrypt)(testData, testKey);
|
||||
const result2 = (0, crypto_1.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 = (0, crypto_1.decrypt)(result1.encrypted, testKey, result1.iv, result1.tag);
|
||||
const decrypted2 = (0, crypto_1.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 = (0, crypto_1.sha256Hash)(data);
|
||||
const hash2 = (0, crypto_1.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 = (0, crypto_1.sha256Hash)('data 1');
|
||||
const hash2 = (0, crypto_1.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 = (0, crypto_1.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 = (0, crypto_1.generateHMAC)(testData, testSecret);
|
||||
const isValid = (0, crypto_1.verifyHMAC)(testData, signature, testSecret);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
test('verifyHMAC rejects incorrect signature', () => {
|
||||
const signature = (0, crypto_1.generateHMAC)(testData, testSecret);
|
||||
const isValid = (0, crypto_1.verifyHMAC)(testData, signature + 'tampered', testSecret);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
test('verifyHMAC rejects signature with wrong secret', () => {
|
||||
const signature = (0, crypto_1.generateHMAC)(testData, testSecret);
|
||||
const isValid = (0, crypto_1.verifyHMAC)(testData, signature, 'wrong secret');
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
test('HMAC is consistent for same inputs', () => {
|
||||
const signature1 = (0, crypto_1.generateHMAC)(testData, testSecret);
|
||||
const signature2 = (0, crypto_1.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 = (0, crypto_1.deriveTenantKey)(masterKey, tenantId);
|
||||
const key2 = (0, crypto_1.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 = (0, crypto_1.deriveTenantKey)(masterKey, 'tenant-1');
|
||||
const key2 = (0, crypto_1.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 = (0, crypto_1.deriveTenantKey)(masterKey, tenantId);
|
||||
const key2 = (0, crypto_1.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 = (0, crypto_1.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 = (0, crypto_1.encryptForDatabase)(testData, testKey);
|
||||
const decrypted = (0, crypto_1.decryptFromDatabase)(encrypted, testKey);
|
||||
expect(decrypted).toEqual(testData);
|
||||
});
|
||||
test('decryptFromDatabase fails with wrong key', () => {
|
||||
const encrypted = (0, crypto_1.encryptForDatabase)(testData, testKey);
|
||||
const wrongKey = 'b'.repeat(64);
|
||||
expect(() => {
|
||||
(0, crypto_1.decryptFromDatabase)(encrypted, wrongKey);
|
||||
}).toThrow();
|
||||
});
|
||||
test('decryptFromDatabase fails with invalid format', () => {
|
||||
expect(() => {
|
||||
(0, crypto_1.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 = (0, crypto_1.encryptForDatabase)(complexData, testKey);
|
||||
const decrypted = (0, crypto_1.decryptFromDatabase)(encrypted, testKey);
|
||||
expect(decrypted).toEqual(complexData);
|
||||
});
|
||||
});
|
||||
describe('Password Generation', () => {
|
||||
test('generateSecurePassword creates password of correct length', () => {
|
||||
const password = (0, crypto_1.generateSecurePassword)(16);
|
||||
expect(typeof password).toBe('string');
|
||||
expect(password.length).toBe(16);
|
||||
});
|
||||
test('generateSecurePassword uses default length', () => {
|
||||
const password = (0, crypto_1.generateSecurePassword)();
|
||||
expect(password.length).toBe(16); // Default length
|
||||
});
|
||||
test('generateSecurePassword creates different passwords', () => {
|
||||
const password1 = (0, crypto_1.generateSecurePassword)();
|
||||
const password2 = (0, crypto_1.generateSecurePassword)();
|
||||
expect(password1).not.toBe(password2);
|
||||
});
|
||||
test('generateSecurePassword includes variety of characters', () => {
|
||||
const password = (0, crypto_1.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 = (0, crypto_1.generateSecurePassword)(12);
|
||||
expect(password.length).toBe(12);
|
||||
expect(/[a-zA-Z0-9!@#$%^&*]/.test(password)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
3
packages/utils/dist/__tests__/setup.d.ts
vendored
Normal file
3
packages/utils/dist/__tests__/setup.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* Test setup for utility functions
|
||||
*/
|
||||
20
packages/utils/dist/__tests__/setup.js
vendored
Normal file
20
packages/utils/dist/__tests__/setup.js
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
/**
|
||||
* 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) => {
|
||||
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);
|
||||
1
packages/utils/dist/__tests__/validation.test.d.ts
vendored
Normal file
1
packages/utils/dist/__tests__/validation.test.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
279
packages/utils/dist/__tests__/validation.test.js
vendored
Normal file
279
packages/utils/dist/__tests__/validation.test.js
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
/**
|
||||
* Unit tests for validation utilities
|
||||
*/
|
||||
const validation_1 = require("../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((0, validation_1.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((0, validation_1.isValidEmail)(email)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Domain Validation', () => {
|
||||
test('validates correct domain formats', () => {
|
||||
const validDomains = [
|
||||
'acme',
|
||||
'test-company',
|
||||
'company123',
|
||||
'a1b2c3',
|
||||
'long-domain-name-with-dashes'
|
||||
];
|
||||
validDomains.forEach(domain => {
|
||||
expect((0, validation_1.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((0, validation_1.isValidDomain)(domain)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Password Validation', () => {
|
||||
test('validates strong passwords', () => {
|
||||
const strongPasswords = [
|
||||
'StrongPass123!',
|
||||
'MySecure#Password1',
|
||||
'Complex$Password99'
|
||||
];
|
||||
strongPasswords.forEach(password => {
|
||||
const result = (0, validation_1.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 = (0, validation_1.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-company',
|
||||
template: 'basic',
|
||||
max_users: 50,
|
||||
resource_limits: {
|
||||
cpu: '1000m',
|
||||
memory: '2Gi',
|
||||
storage: '10Gi'
|
||||
}
|
||||
};
|
||||
test('validates correct tenant request', () => {
|
||||
const result = (0, validation_1.validateTenantCreateRequest)(validTenantRequest);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
test('rejects request with missing name', () => {
|
||||
const request = { ...validTenantRequest, name: '' };
|
||||
const result = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 assistant.',
|
||||
context_sources: ['doc1', 'doc2']
|
||||
};
|
||||
test('validates correct chat request', () => {
|
||||
const result = (0, validation_1.validateChatRequest)(validChatRequest);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
test('rejects request with empty message', () => {
|
||||
const request = { ...validChatRequest, message: '' };
|
||||
const result = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.validateChatRequest)(request);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toContain('2000 characters or less');
|
||||
});
|
||||
});
|
||||
describe('Document Upload Validation', () => {
|
||||
const createMockFile = (size, type, name) => ({
|
||||
file: Buffer.alloc(size),
|
||||
filename: name,
|
||||
file_type: type
|
||||
});
|
||||
test('validates correct document upload', () => {
|
||||
const upload = createMockFile(1000, 'text/plain', 'test.txt');
|
||||
const result = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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 = (0, validation_1.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((0, validation_1.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((0, validation_1.isValidUUID)(uuid)).toBe(false);
|
||||
});
|
||||
});
|
||||
test('validatePagination normalizes and validates parameters', () => {
|
||||
// Test valid parameters
|
||||
const result1 = (0, validation_1.validatePagination)(2, 50);
|
||||
expect(result1.page).toBe(2);
|
||||
expect(result1.limit).toBe(50);
|
||||
expect(result1.errors).toHaveLength(0);
|
||||
// Test defaults
|
||||
const result2 = (0, validation_1.validatePagination)();
|
||||
expect(result2.page).toBe(1);
|
||||
expect(result2.limit).toBe(20);
|
||||
// Test invalid parameters
|
||||
const result3 = (0, validation_1.validatePagination)(-1, 150);
|
||||
expect(result3.page).toBe(1); // Corrected
|
||||
expect(result3.limit).toBe(100); // Corrected to max
|
||||
expect(result3.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
packages/utils/dist/auth.d.ts
vendored
Normal file
49
packages/utils/dist/auth.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { JWTPayload, Capability } from '@gt2/types';
|
||||
/**
|
||||
* Generate a cryptographic hash for capability verification
|
||||
*/
|
||||
export declare function generateCapabilityHash(capabilities: Capability[]): string;
|
||||
/**
|
||||
* Verify capability hash to ensure JWT hasn't been tampered with
|
||||
*/
|
||||
export declare function verifyCapabilityHash(capabilities: Capability[], hash: string): boolean;
|
||||
/**
|
||||
* Create a capability-based JWT token
|
||||
*/
|
||||
export declare function createJWT(payload: Omit<JWTPayload, 'capability_hash' | 'exp' | 'iat'>): string;
|
||||
/**
|
||||
* Verify and decode a JWT token
|
||||
*/
|
||||
export declare function verifyJWT(token: string): JWTPayload | null;
|
||||
/**
|
||||
* Check if user has required capability
|
||||
*/
|
||||
export declare function hasCapability(userCapabilities: Capability[], resource: string, action: string): boolean;
|
||||
/**
|
||||
* Hash password for storage
|
||||
*/
|
||||
export declare function hashPassword(password: string): Promise<string>;
|
||||
/**
|
||||
* Verify password against hash
|
||||
*/
|
||||
export declare function verifyPassword(password: string, hash: string): Promise<boolean>;
|
||||
/**
|
||||
* Generate secure random token
|
||||
*/
|
||||
export declare function generateSecureToken(length?: number): string;
|
||||
/**
|
||||
* Create tenant-scoped capabilities
|
||||
*/
|
||||
export declare function createTenantCapabilities(tenantDomain: string, userType: 'tenant_admin' | 'tenant_user'): Capability[];
|
||||
/**
|
||||
* Create super admin capabilities
|
||||
*/
|
||||
export declare function createSuperAdminCapabilities(): Capability[];
|
||||
/**
|
||||
* Extract Bearer token from Authorization header
|
||||
*/
|
||||
export declare function extractBearerToken(authHeader?: string): string | null;
|
||||
/**
|
||||
* Check if JWT token is expired
|
||||
*/
|
||||
export declare function isTokenExpired(token: JWTPayload): boolean;
|
||||
187
packages/utils/dist/auth.js
vendored
Normal file
187
packages/utils/dist/auth.js
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateCapabilityHash = generateCapabilityHash;
|
||||
exports.verifyCapabilityHash = verifyCapabilityHash;
|
||||
exports.createJWT = createJWT;
|
||||
exports.verifyJWT = verifyJWT;
|
||||
exports.hasCapability = hasCapability;
|
||||
exports.hashPassword = hashPassword;
|
||||
exports.verifyPassword = verifyPassword;
|
||||
exports.generateSecureToken = generateSecureToken;
|
||||
exports.createTenantCapabilities = createTenantCapabilities;
|
||||
exports.createSuperAdminCapabilities = createSuperAdminCapabilities;
|
||||
exports.extractBearerToken = extractBearerToken;
|
||||
exports.isTokenExpired = isTokenExpired;
|
||||
// Authentication and Authorization Utilities
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const bcryptjs_1 = __importDefault(require("bcryptjs"));
|
||||
const crypto_1 = __importDefault(require("crypto"));
|
||||
// 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
|
||||
*/
|
||||
function generateCapabilityHash(capabilities) {
|
||||
const capabilityString = JSON.stringify(capabilities, Object.keys(capabilities).sort());
|
||||
return crypto_1.default.createHmac('sha256', JWT_SECRET).update(capabilityString).digest('hex');
|
||||
}
|
||||
/**
|
||||
* Verify capability hash to ensure JWT hasn't been tampered with
|
||||
*/
|
||||
function verifyCapabilityHash(capabilities, hash) {
|
||||
const expectedHash = generateCapabilityHash(capabilities);
|
||||
return crypto_1.default.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
|
||||
}
|
||||
/**
|
||||
* Create a capability-based JWT token
|
||||
*/
|
||||
function createJWT(payload) {
|
||||
const capability_hash = generateCapabilityHash(payload.capabilities);
|
||||
const fullPayload = {
|
||||
...payload,
|
||||
capability_hash,
|
||||
exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60), // 24 hours
|
||||
iat: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
return jsonwebtoken_1.default.sign(fullPayload, JWT_SECRET, { algorithm: 'HS256' });
|
||||
}
|
||||
/**
|
||||
* Verify and decode a JWT token
|
||||
*/
|
||||
function verifyJWT(token) {
|
||||
try {
|
||||
const decoded = jsonwebtoken_1.default.verify(token, JWT_SECRET);
|
||||
// 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
|
||||
*/
|
||||
function hasCapability(userCapabilities, resource, action) {
|
||||
return userCapabilities.some(cap => {
|
||||
// Check if capability matches resource (support wildcards)
|
||||
const resourceMatch = cap.resource === '*' ||
|
||||
cap.resource === resource ||
|
||||
resource.startsWith(cap.resource.replace('*', ''));
|
||||
// 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
|
||||
*/
|
||||
async function hashPassword(password) {
|
||||
const salt = await bcryptjs_1.default.genSalt(12);
|
||||
return bcryptjs_1.default.hash(password, salt);
|
||||
}
|
||||
/**
|
||||
* Verify password against hash
|
||||
*/
|
||||
async function verifyPassword(password, hash) {
|
||||
return bcryptjs_1.default.compare(password, hash);
|
||||
}
|
||||
/**
|
||||
* Generate secure random token
|
||||
*/
|
||||
function generateSecureToken(length = 32) {
|
||||
return crypto_1.default.randomBytes(length).toString('hex');
|
||||
}
|
||||
/**
|
||||
* Create tenant-scoped capabilities
|
||||
*/
|
||||
function createTenantCapabilities(tenantDomain, userType) {
|
||||
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
|
||||
*/
|
||||
function createSuperAdminCapabilities() {
|
||||
return [
|
||||
{
|
||||
resource: '*',
|
||||
actions: ['*'],
|
||||
constraints: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Extract Bearer token from Authorization header
|
||||
*/
|
||||
function extractBearerToken(authHeader) {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return null;
|
||||
}
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
/**
|
||||
* Check if JWT token is expired
|
||||
*/
|
||||
function isTokenExpired(token) {
|
||||
return Date.now() >= token.exp * 1000;
|
||||
}
|
||||
44
packages/utils/dist/crypto.d.ts
vendored
Normal file
44
packages/utils/dist/crypto.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Generate a random encryption key
|
||||
*/
|
||||
export declare function generateEncryptionKey(): string;
|
||||
/**
|
||||
* Encrypt data using AES-256-GCM
|
||||
*/
|
||||
export declare function encrypt(data: string, keyHex: string): {
|
||||
encrypted: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
};
|
||||
/**
|
||||
* Decrypt data using AES-256-GCM
|
||||
*/
|
||||
export declare function decrypt(encryptedData: string, keyHex: string, ivHex: string, tagHex: string): string;
|
||||
/**
|
||||
* Hash data using SHA-256
|
||||
*/
|
||||
export declare function sha256Hash(data: string): string;
|
||||
/**
|
||||
* Generate HMAC signature
|
||||
*/
|
||||
export declare function generateHMAC(data: string, secret: string): string;
|
||||
/**
|
||||
* Verify HMAC signature
|
||||
*/
|
||||
export declare function verifyHMAC(data: string, signature: string, secret: string): boolean;
|
||||
/**
|
||||
* Generate tenant-specific encryption key from master key and tenant ID
|
||||
*/
|
||||
export declare function deriveTenantKey(masterKey: string, tenantId: string): string;
|
||||
/**
|
||||
* Encrypt JSON data for database storage
|
||||
*/
|
||||
export declare function encryptForDatabase(data: any, encryptionKey: string): string;
|
||||
/**
|
||||
* Decrypt JSON data from database storage
|
||||
*/
|
||||
export declare function decryptFromDatabase(encryptedData: string, encryptionKey: string): any;
|
||||
/**
|
||||
* Generate a secure random password
|
||||
*/
|
||||
export declare function generateSecurePassword(length?: number): string;
|
||||
118
packages/utils/dist/crypto.js
vendored
Normal file
118
packages/utils/dist/crypto.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateEncryptionKey = generateEncryptionKey;
|
||||
exports.encrypt = encrypt;
|
||||
exports.decrypt = decrypt;
|
||||
exports.sha256Hash = sha256Hash;
|
||||
exports.generateHMAC = generateHMAC;
|
||||
exports.verifyHMAC = verifyHMAC;
|
||||
exports.deriveTenantKey = deriveTenantKey;
|
||||
exports.encryptForDatabase = encryptForDatabase;
|
||||
exports.decryptFromDatabase = decryptFromDatabase;
|
||||
exports.generateSecurePassword = generateSecurePassword;
|
||||
// Cryptographic utilities for GT 2.0
|
||||
const crypto_1 = __importDefault(require("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
|
||||
*/
|
||||
function generateEncryptionKey() {
|
||||
return crypto_1.default.randomBytes(KEY_LENGTH).toString('hex');
|
||||
}
|
||||
/**
|
||||
* Encrypt data using AES-256-GCM
|
||||
*/
|
||||
function encrypt(data, keyHex) {
|
||||
const key = Buffer.from(keyHex, 'hex');
|
||||
const iv = crypto_1.default.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto_1.default.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
|
||||
*/
|
||||
function decrypt(encryptedData, keyHex, ivHex, tagHex) {
|
||||
const key = Buffer.from(keyHex, 'hex');
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const tag = Buffer.from(tagHex, 'hex');
|
||||
const decipher = crypto_1.default.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
|
||||
*/
|
||||
function sha256Hash(data) {
|
||||
return crypto_1.default.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
/**
|
||||
* Generate HMAC signature
|
||||
*/
|
||||
function generateHMAC(data, secret) {
|
||||
return crypto_1.default.createHmac('sha256', secret).update(data).digest('hex');
|
||||
}
|
||||
/**
|
||||
* Verify HMAC signature
|
||||
*/
|
||||
function verifyHMAC(data, signature, secret) {
|
||||
const expectedSignature = generateHMAC(data, secret);
|
||||
return crypto_1.default.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'));
|
||||
}
|
||||
/**
|
||||
* Generate tenant-specific encryption key from master key and tenant ID
|
||||
*/
|
||||
function deriveTenantKey(masterKey, tenantId) {
|
||||
const key = crypto_1.default.pbkdf2Sync(tenantId, Buffer.from(masterKey, 'hex'), 100000, // iterations
|
||||
KEY_LENGTH, 'sha256');
|
||||
return key.toString('hex');
|
||||
}
|
||||
/**
|
||||
* Encrypt JSON data for database storage
|
||||
*/
|
||||
function encryptForDatabase(data, encryptionKey) {
|
||||
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
|
||||
*/
|
||||
function decryptFromDatabase(encryptedData, encryptionKey) {
|
||||
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
|
||||
*/
|
||||
function generateSecurePassword(length = 16) {
|
||||
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = crypto_1.default.randomInt(0, charset.length);
|
||||
password += charset[randomIndex];
|
||||
}
|
||||
return password;
|
||||
}
|
||||
51
packages/utils/dist/database.d.ts
vendored
Normal file
51
packages/utils/dist/database.d.ts
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Generate SQLite database path for tenant
|
||||
*/
|
||||
export declare function getTenantDatabasePath(tenantDomain: string, dataDir?: string): string;
|
||||
/**
|
||||
* Generate ChromaDB collection name for tenant
|
||||
*/
|
||||
export declare function getTenantChromaCollection(tenantDomain: string): string;
|
||||
/**
|
||||
* Generate Redis key prefix for tenant
|
||||
*/
|
||||
export declare function getTenantRedisPrefix(tenantDomain: string): string;
|
||||
/**
|
||||
* Generate MinIO bucket name for tenant
|
||||
*/
|
||||
export declare function getTenantMinioBucket(tenantDomain: string): string;
|
||||
/**
|
||||
* Generate SQLite WAL mode configuration
|
||||
*/
|
||||
export declare function getSQLiteWALConfig(): string;
|
||||
/**
|
||||
* Generate SQLite encryption configuration
|
||||
*/
|
||||
export declare function getSQLiteEncryptionConfig(encryptionKey: string): string;
|
||||
/**
|
||||
* Create tenant database schema (SQLite)
|
||||
*/
|
||||
export declare function getTenantDatabaseSchema(): string;
|
||||
/**
|
||||
* Generate unique document chunk ID
|
||||
*/
|
||||
export declare function generateDocumentChunkId(documentId: number, chunkIndex: number): string;
|
||||
/**
|
||||
* Parse connection string for database configuration
|
||||
*/
|
||||
export declare function parseConnectionString(connectionString: string): {
|
||||
host?: string;
|
||||
port?: number;
|
||||
database?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
options?: Record<string, string>;
|
||||
};
|
||||
/**
|
||||
* Escape SQL identifiers (table names, column names, etc.)
|
||||
*/
|
||||
export declare function escapeSQLIdentifier(identifier: string): string;
|
||||
/**
|
||||
* Generate database backup filename
|
||||
*/
|
||||
export declare function generateBackupFilename(tenantDomain: string, timestamp?: Date): string;
|
||||
216
packages/utils/dist/database.js
vendored
Normal file
216
packages/utils/dist/database.js
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
"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`;
|
||||
}
|
||||
1
packages/utils/dist/index.d.ts
vendored
Normal file
1
packages/utils/dist/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
1
packages/utils/dist/index.js
vendored
Normal file
1
packages/utils/dist/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
58
packages/utils/dist/tenant.d.ts
vendored
Normal file
58
packages/utils/dist/tenant.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Tenant, TenantCreateRequest } from '@gt2/types';
|
||||
/**
|
||||
* Generate Kubernetes namespace name for tenant
|
||||
*/
|
||||
export declare function generateTenantNamespace(domain: string): string;
|
||||
/**
|
||||
* Generate tenant subdomain
|
||||
*/
|
||||
export declare function generateTenantSubdomain(domain: string): string;
|
||||
/**
|
||||
* Generate OS user ID for tenant isolation
|
||||
*/
|
||||
export declare function generateTenantUserId(tenantId: number): number;
|
||||
/**
|
||||
* Generate OS group ID for tenant isolation
|
||||
*/
|
||||
export declare function generateTenantGroupId(tenantId: number): number;
|
||||
/**
|
||||
* Get tenant data directory path
|
||||
*/
|
||||
export declare function getTenantDataPath(domain: string, baseDataDir?: string): string;
|
||||
/**
|
||||
* Get default resource limits based on template
|
||||
*/
|
||||
export declare function getTemplateResourceLimits(template: string): {
|
||||
cpu: string;
|
||||
memory: string;
|
||||
storage: string;
|
||||
};
|
||||
/**
|
||||
* Get default max users based on template
|
||||
*/
|
||||
export declare function getTemplateMaxUsers(template: string): number;
|
||||
/**
|
||||
* Validate tenant domain availability (placeholder - would check database in real implementation)
|
||||
*/
|
||||
export declare function isDomainAvailable(domain: string): boolean;
|
||||
/**
|
||||
* Generate complete tenant configuration from create request
|
||||
*/
|
||||
export declare function generateTenantConfig(request: TenantCreateRequest, masterEncryptionKey: string): Partial<Tenant>;
|
||||
/**
|
||||
* Generate Kubernetes deployment YAML for tenant
|
||||
*/
|
||||
export declare function generateTenantDeploymentYAML(tenant: Tenant, tenantUserId: number): string;
|
||||
/**
|
||||
* Calculate tenant usage costs
|
||||
*/
|
||||
export declare function calculateTenantCosts(cpuUsage: number, // CPU hours
|
||||
memoryUsage: number, // Memory GB-hours
|
||||
storageUsage: number, // Storage GB-hours
|
||||
aiTokens: number): {
|
||||
cpu_cost_cents: number;
|
||||
memory_cost_cents: number;
|
||||
storage_cost_cents: number;
|
||||
ai_cost_cents: number;
|
||||
total_cost_cents: number;
|
||||
};
|
||||
338
packages/utils/dist/tenant.js
vendored
Normal file
338
packages/utils/dist/tenant.js
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateTenantNamespace = generateTenantNamespace;
|
||||
exports.generateTenantSubdomain = generateTenantSubdomain;
|
||||
exports.generateTenantUserId = generateTenantUserId;
|
||||
exports.generateTenantGroupId = generateTenantGroupId;
|
||||
exports.getTenantDataPath = getTenantDataPath;
|
||||
exports.getTemplateResourceLimits = getTemplateResourceLimits;
|
||||
exports.getTemplateMaxUsers = getTemplateMaxUsers;
|
||||
exports.isDomainAvailable = isDomainAvailable;
|
||||
exports.generateTenantConfig = generateTenantConfig;
|
||||
exports.generateTenantDeploymentYAML = generateTenantDeploymentYAML;
|
||||
exports.calculateTenantCosts = calculateTenantCosts;
|
||||
/**
|
||||
* Generate Kubernetes namespace name for tenant
|
||||
*/
|
||||
function generateTenantNamespace(domain) {
|
||||
return `gt-${domain}`;
|
||||
}
|
||||
/**
|
||||
* Generate tenant subdomain
|
||||
*/
|
||||
function generateTenantSubdomain(domain) {
|
||||
return domain; // For now, subdomain matches domain
|
||||
}
|
||||
/**
|
||||
* Generate OS user ID for tenant isolation
|
||||
*/
|
||||
function generateTenantUserId(tenantId) {
|
||||
const baseUserId = 10000; // Start user IDs from 10000
|
||||
return baseUserId + tenantId;
|
||||
}
|
||||
/**
|
||||
* Generate OS group ID for tenant isolation
|
||||
*/
|
||||
function generateTenantGroupId(tenantId) {
|
||||
return generateTenantUserId(tenantId); // Use same ID for group
|
||||
}
|
||||
/**
|
||||
* Get tenant data directory path
|
||||
*/
|
||||
function getTenantDataPath(domain, baseDataDir = '/data') {
|
||||
return `${baseDataDir}/${domain}`;
|
||||
}
|
||||
/**
|
||||
* Get default resource limits based on template
|
||||
*/
|
||||
function getTemplateResourceLimits(template) {
|
||||
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
|
||||
*/
|
||||
function getTemplateMaxUsers(template) {
|
||||
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)
|
||||
*/
|
||||
function isDomainAvailable(domain) {
|
||||
// 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
|
||||
*/
|
||||
function generateTenantConfig(request, masterEncryptionKey) {
|
||||
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
|
||||
*/
|
||||
function generateTenantDeploymentYAML(tenant, tenantUserId) {
|
||||
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"
|
||||
REDIS_PREFIX: "gt2:${tenant.domain}:"
|
||||
MINIO_BUCKET: "gt2-${tenant.domain}-files"
|
||||
---
|
||||
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
|
||||
*/
|
||||
function calculateTenantCosts(cpuUsage, // CPU hours
|
||||
memoryUsage, // Memory GB-hours
|
||||
storageUsage, // Storage GB-hours
|
||||
aiTokens // AI tokens used
|
||||
) {
|
||||
// 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
|
||||
};
|
||||
}
|
||||
53
packages/utils/dist/validation.d.ts
vendored
Normal file
53
packages/utils/dist/validation.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import { TenantCreateRequest, ChatRequest, DocumentUploadRequest } from '@gt2/types';
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
export declare function isValidEmail(email: string): boolean;
|
||||
/**
|
||||
* Validate domain name format
|
||||
*/
|
||||
export declare function isValidDomain(domain: string): boolean;
|
||||
/**
|
||||
* Validate password strength
|
||||
*/
|
||||
export declare function isValidPassword(password: string): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
/**
|
||||
* Validate tenant creation request
|
||||
*/
|
||||
export declare function validateTenantCreateRequest(request: TenantCreateRequest): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
/**
|
||||
* Validate chat request
|
||||
*/
|
||||
export declare function validateChatRequest(request: ChatRequest): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
/**
|
||||
* Validate file upload request
|
||||
*/
|
||||
export declare function validateDocumentUpload(request: DocumentUploadRequest): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
/**
|
||||
* Sanitize string input to prevent injection attacks
|
||||
*/
|
||||
export declare function sanitizeString(input: string): string;
|
||||
/**
|
||||
* Validate UUID format
|
||||
*/
|
||||
export declare function isValidUUID(uuid: string): boolean;
|
||||
/**
|
||||
* Validate pagination parameters
|
||||
*/
|
||||
export declare function validatePagination(page?: number, limit?: number): {
|
||||
page: number;
|
||||
limit: number;
|
||||
errors: string[];
|
||||
};
|
||||
205
packages/utils/dist/validation.js
vendored
Normal file
205
packages/utils/dist/validation.js
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isValidEmail = isValidEmail;
|
||||
exports.isValidDomain = isValidDomain;
|
||||
exports.isValidPassword = isValidPassword;
|
||||
exports.validateTenantCreateRequest = validateTenantCreateRequest;
|
||||
exports.validateChatRequest = validateChatRequest;
|
||||
exports.validateDocumentUpload = validateDocumentUpload;
|
||||
exports.sanitizeString = sanitizeString;
|
||||
exports.isValidUUID = isValidUUID;
|
||||
exports.validatePagination = validatePagination;
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
/**
|
||||
* Validate domain name format
|
||||
*/
|
||||
function isValidDomain(domain) {
|
||||
// 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
|
||||
*/
|
||||
function isValidPassword(password) {
|
||||
const errors = [];
|
||||
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
|
||||
*/
|
||||
function validateTenantCreateRequest(request) {
|
||||
const errors = [];
|
||||
// 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
|
||||
*/
|
||||
function validateChatRequest(request) {
|
||||
const errors = [];
|
||||
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
|
||||
*/
|
||||
function validateDocumentUpload(request) {
|
||||
const errors = [];
|
||||
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
|
||||
*/
|
||||
function sanitizeString(input) {
|
||||
return input
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove script tags
|
||||
.replace(/javascript:/gi, '') // Remove javascript: protocol
|
||||
.replace(/on\w+\s*=\s*['"][^'"]*['"]?/gi, '') // Remove event handlers
|
||||
.trim();
|
||||
}
|
||||
/**
|
||||
* Validate UUID format
|
||||
*/
|
||||
function isValidUUID(uuid) {
|
||||
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
|
||||
*/
|
||||
function validatePagination(page, limit) {
|
||||
const errors = [];
|
||||
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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user