GT AI OS Community v2.0.33 - Add NVIDIA NIM and Nemotron agents

- Updated python_coding_microproject.csv to use NVIDIA NIM Kimi K2
- Updated kali_linux_shell_simulator.csv to use NVIDIA NIM Kimi K2
  - Made more general-purpose (flexible targets, expanded tools)
- Added nemotron-mini-agent.csv for fast local inference via Ollama
- Added nemotron-agent.csv for advanced reasoning via Ollama
- Added wiki page: Projects for NVIDIA NIMs and Nemotron
This commit is contained in:
HackWeasel
2025-12-12 17:47:14 -05:00
commit 310491a557
750 changed files with 232701 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
"""
Setup configuration for GT 2.0 API Standards package
"""
from setuptools import setup, find_packages
setup(
name="gt2-api-standards",
version="1.0.0",
description="GT 2.0 Capability-Based REST (CB-REST) API Standards",
author="GT Edge AI",
author_email="engineering@gtedgeai.com",
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires=">=3.11",
install_requires=[
"fastapi>=0.104.0",
"pydantic>=2.0.0",
"pyjwt>=2.8.0",
"python-jose[cryptography]>=3.3.0",
],
extras_require={
"dev": [
"pytest>=7.4.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.1.0",
"black>=23.0.0",
"mypy>=1.5.0",
]
},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
)

View File

@@ -0,0 +1,48 @@
"""
GT 2.0 API Standards - Capability-Based REST (CB-REST)
A simple, secure API standard designed for GT 2.0's philosophy of
"Elegant Simplicity Through Intelligent Architecture"
"""
from .response import StandardResponse, StandardError, format_response, format_error
from .capability import (
Capability,
CapabilityVerifier,
verify_capability,
require_capability,
extract_capability_from_jwt
)
from .errors import ErrorCode, APIError, error_responses
from .middleware import (
CapabilityMiddleware,
RequestCorrelationMiddleware,
TenantIsolationMiddleware
)
__all__ = [
# Response formatting
'StandardResponse',
'StandardError',
'format_response',
'format_error',
# Capability verification
'Capability',
'CapabilityVerifier',
'verify_capability',
'require_capability',
'extract_capability_from_jwt',
# Error handling
'ErrorCode',
'APIError',
'error_responses',
# Middleware
'CapabilityMiddleware',
'RequestCorrelationMiddleware',
'TenantIsolationMiddleware'
]
__version__ = '1.0.0'

View File

@@ -0,0 +1,337 @@
"""
Capability-based access control for GT 2.0 CB-REST API
Design principle: Security through cryptographic capabilities embedded in JWT tokens
Perfect tenant isolation through capability scoping
"""
import hashlib
import hmac
import json
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
import jwt
from pydantic import BaseModel, Field
from fastapi import Depends, HTTPException, Request, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .errors import ErrorCode, APIError
# Security scheme for FastAPI
security = HTTPBearer()
class Capability(BaseModel):
"""
A single capability grant
Format: resource:id:action
Example: tenant:customer1:read
"""
resource: str = Field(..., description="Resource type (tenant, user, resource, etc.)")
resource_id: str = Field(..., description="Specific resource ID or wildcard (*)")
action: str = Field(..., description="Action allowed (read, write, create, delete, admin)")
constraints: Optional[Dict[str, Any]] = Field(None, description="Additional constraints")
expires_at: Optional[datetime] = Field(None, description="Expiration time for this capability")
def matches(self, required_resource: str, required_id: str, required_action: str) -> bool:
"""
Check if this capability matches the required permission
Supports wildcards (*) for resource_id and action
"""
# Check resource type
if self.resource != required_resource and self.resource != "*":
return False
# Check resource ID (supports wildcards)
if self.resource_id != required_id and self.resource_id != "*":
return False
# Check action (supports wildcards and hierarchical permissions)
if self.action == "*" or self.action == "admin":
return True # Admin has all permissions
if self.action == required_action:
return True
# Hierarchical permissions: write includes read, delete includes write
action_hierarchy = {
"delete": ["write", "read"],
"write": ["read"],
"create": ["read"]
}
if required_action in action_hierarchy.get(self.action, []):
return True
return False
def to_string(self) -> str:
"""Convert capability to string format"""
return f"{self.resource}:{self.resource_id}:{self.action}"
@classmethod
def from_string(cls, capability_str: str) -> "Capability":
"""Parse capability from string format"""
parts = capability_str.split(":")
if len(parts) != 3:
raise ValueError(f"Invalid capability format: {capability_str}")
return cls(
resource=parts[0],
resource_id=parts[1],
action=parts[2]
)
class CapabilityToken(BaseModel):
"""
JWT token payload with embedded capabilities
"""
sub: str = Field(..., description="Subject (user email or ID)")
tenant_id: str = Field(..., description="Tenant ID for isolation")
user_type: str = Field(..., description="User type (super_admin, gt_admin, tenant_admin, tenant_user)")
capabilities: List[Dict[str, Any]] = Field(..., description="List of granted capabilities")
iat: int = Field(..., description="Issued at timestamp")
exp: int = Field(..., description="Expiration timestamp")
jti: Optional[str] = Field(None, description="JWT ID for revocation")
def get_capabilities(self) -> List[Capability]:
"""Convert capability dicts to Capability objects"""
return [Capability(**cap) for cap in self.capabilities]
def has_capability(self, resource: str, resource_id: str, action: str) -> bool:
"""Check if token has a specific capability"""
for cap in self.get_capabilities():
if cap.matches(resource, resource_id, action):
# Check expiration if set
if cap.expires_at and cap.expires_at < datetime.utcnow():
continue
return True
return False
class CapabilityVerifier:
"""
Verifies capabilities and signatures
"""
def __init__(self, secret_key: str, algorithm: str = "HS256"):
self.secret_key = secret_key
self.algorithm = algorithm
def verify_token(self, token: str) -> CapabilityToken:
"""
Verify and decode a JWT capability token
Args:
token: JWT token string
Returns:
Decoded CapabilityToken
Raises:
APIError: If token is invalid or expired
"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
return CapabilityToken(**payload)
except jwt.ExpiredSignatureError:
raise APIError(
code=ErrorCode.CAPABILITY_EXPIRED,
message="Capability token has expired",
status_code=401
)
except jwt.InvalidTokenError as e:
raise APIError(
code=ErrorCode.CAPABILITY_INVALID,
message=f"Invalid capability token: {str(e)}",
status_code=401
)
def verify_signature(self, token: str, signature: str) -> bool:
"""
Verify HMAC signature of capability
Args:
token: JWT token string
signature: HMAC signature to verify
Returns:
True if signature is valid
"""
expected_signature = hmac.new(
self.secret_key.encode(),
token.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
def create_token(
self,
user_email: str,
tenant_id: str,
user_type: str,
capabilities: List[Capability],
expires_in: timedelta = timedelta(hours=24)
) -> str:
"""
Create a new capability token
Args:
user_email: User's email address
tenant_id: Tenant ID for isolation
user_type: Type of user
capabilities: List of capabilities to grant
expires_in: Token expiration time
Returns:
JWT token string
"""
now = datetime.utcnow()
exp = now + expires_in
payload = CapabilityToken(
sub=user_email,
tenant_id=tenant_id,
user_type=user_type,
capabilities=[cap.dict(exclude_none=True) for cap in capabilities],
iat=int(now.timestamp()),
exp=int(exp.timestamp())
)
return jwt.encode(
payload.dict(exclude_none=True),
self.secret_key,
algorithm=self.algorithm
)
# Global verifier instance (initialized by application)
_verifier: Optional[CapabilityVerifier] = None
def init_capability_verifier(secret_key: str, algorithm: str = "HS256"):
"""Initialize the global capability verifier"""
global _verifier
_verifier = CapabilityVerifier(secret_key, algorithm)
def get_verifier() -> CapabilityVerifier:
"""Get the global capability verifier"""
if _verifier is None:
raise RuntimeError("Capability verifier not initialized. Call init_capability_verifier first.")
return _verifier
async def verify_capability(
credentials: HTTPAuthorizationCredentials = Security(security),
request: Request = None
) -> CapabilityToken:
"""
FastAPI dependency to verify capability token
Args:
credentials: Bearer token from Authorization header
request: Optional request object for additional context
Returns:
Verified CapabilityToken
Raises:
HTTPException: If token is invalid
"""
verifier = get_verifier()
# Check for signature header if request provided
signature = None
if request:
signature = request.headers.get("X-Capability-Signature")
if signature and not verifier.verify_signature(credentials.credentials, signature):
raise APIError(
code=ErrorCode.CAPABILITY_SIGNATURE_INVALID,
message="Capability signature verification failed",
status_code=401
)
return verifier.verify_token(credentials.credentials)
def require_capability(
resource: str,
resource_id: str,
action: str
):
"""
Create a FastAPI dependency that requires a specific capability
Args:
resource: Resource type
resource_id: Resource ID (can be "*" for wildcard)
action: Required action
Returns:
FastAPI dependency function
"""
async def capability_checker(
token: CapabilityToken = Depends(verify_capability)
) -> CapabilityToken:
"""Check if the token has the required capability"""
# Super admins bypass all checks (GT 2.0 admin efficiency)
if token.user_type == "super_admin":
return token
# GT admins can access all tenants but not system resources
if token.user_type == "gt_admin" and resource in ["tenant", "user", "resource"]:
return token
# Check specific capability
if not token.has_capability(resource, resource_id, action):
capability_required = f"{resource}:{resource_id}:{action}"
# Find what capabilities the user has for this resource
user_caps = [
cap.to_string()
for cap in token.get_capabilities()
if cap.resource == resource
]
raise APIError(
code=ErrorCode.CAPABILITY_INSUFFICIENT,
message=f"Insufficient capability for {resource} {action}",
status_code=403,
capability_required=capability_required,
capability_provided=", ".join(user_caps) if user_caps else "none"
)
return token
return capability_checker
def extract_capability_from_jwt(token_str: str) -> Optional[CapabilityToken]:
"""
Extract capability token without verification (for logging/debugging)
WARNING: This does not verify the token! Use only for non-security purposes.
Args:
token_str: JWT token string
Returns:
CapabilityToken if decodable, None otherwise
"""
try:
# Decode without verification
payload = jwt.decode(token_str, options={"verify_signature": False})
return CapabilityToken(**payload)
except Exception:
return None

View File

@@ -0,0 +1,207 @@
"""
Standard error codes for GT 2.0 CB-REST API
Design principle: Simple, clear error codes that directly indicate the issue
No complex hierarchies, just straightforward categories
"""
from enum import Enum
from typing import Dict, Any, Optional
from fastapi import HTTPException, status
class ErrorCode(str, Enum):
"""
Standard error codes following GT 2.0 philosophy:
- Simple and clear
- Security-focused
- Tenant-aware
"""
# Capability errors (security by design)
CAPABILITY_INSUFFICIENT = "CAPABILITY_INSUFFICIENT"
CAPABILITY_INVALID = "CAPABILITY_INVALID"
CAPABILITY_EXPIRED = "CAPABILITY_EXPIRED"
CAPABILITY_SIGNATURE_INVALID = "CAPABILITY_SIGNATURE_INVALID"
# Resource errors
RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
RESOURCE_LOCKED = "RESOURCE_LOCKED"
RESOURCE_QUOTA_EXCEEDED = "RESOURCE_QUOTA_EXCEEDED"
# Tenant isolation errors
TENANT_ISOLATED = "TENANT_ISOLATED"
TENANT_NOT_FOUND = "TENANT_NOT_FOUND"
TENANT_SUSPENDED = "TENANT_SUSPENDED"
TENANT_QUOTA_EXCEEDED = "TENANT_QUOTA_EXCEEDED"
# Request errors
INVALID_REQUEST = "INVALID_REQUEST"
VALIDATION_FAILED = "VALIDATION_FAILED"
MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
INVALID_FIELD_VALUE = "INVALID_FIELD_VALUE"
# System errors (minimal, as per GT 2.0 philosophy)
SYSTEM_ERROR = "SYSTEM_ERROR"
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
# File-based database specific errors
FILE_LOCK_TIMEOUT = "FILE_LOCK_TIMEOUT"
FILE_CORRUPTION = "FILE_CORRUPTION"
ENCRYPTION_FAILED = "ENCRYPTION_FAILED"
class APIError(HTTPException):
"""
Standard API error that integrates with FastAPI
Simplifies error handling while maintaining consistency
"""
def __init__(
self,
code: ErrorCode,
message: str,
status_code: int = status.HTTP_400_BAD_REQUEST,
capability_required: Optional[str] = None,
capability_provided: Optional[str] = None,
details: Optional[Dict[str, Any]] = None
):
self.code = code
self.message = message
self.capability_required = capability_required
self.capability_provided = capability_provided
self.details = details or {}
# Create the detail structure for HTTPException
detail = {
"code": code.value,
"message": message,
"capability_required": capability_required,
"capability_provided": capability_provided,
"details": self.details
}
# Remove None values for cleaner response
detail = {k: v for k, v in detail.items() if v is not None}
super().__init__(status_code=status_code, detail=detail)
# Pre-defined error responses for common scenarios
error_responses: Dict[ErrorCode, Dict[str, Any]] = {
ErrorCode.CAPABILITY_INSUFFICIENT: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "User lacks required capability for this operation"
},
ErrorCode.CAPABILITY_INVALID: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Invalid capability token"
},
ErrorCode.CAPABILITY_EXPIRED: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Capability token has expired"
},
ErrorCode.CAPABILITY_SIGNATURE_INVALID: {
"status_code": status.HTTP_401_UNAUTHORIZED,
"description": "Capability signature verification failed"
},
ErrorCode.RESOURCE_NOT_FOUND: {
"status_code": status.HTTP_404_NOT_FOUND,
"description": "Requested resource does not exist"
},
ErrorCode.RESOURCE_ALREADY_EXISTS: {
"status_code": status.HTTP_409_CONFLICT,
"description": "Resource already exists"
},
ErrorCode.RESOURCE_LOCKED: {
"status_code": status.HTTP_423_LOCKED,
"description": "Resource is locked"
},
ErrorCode.RESOURCE_QUOTA_EXCEEDED: {
"status_code": status.HTTP_429_TOO_MANY_REQUESTS,
"description": "Resource quota exceeded"
},
ErrorCode.TENANT_ISOLATED: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "Cross-tenant access attempted"
},
ErrorCode.TENANT_NOT_FOUND: {
"status_code": status.HTTP_404_NOT_FOUND,
"description": "Tenant does not exist"
},
ErrorCode.TENANT_SUSPENDED: {
"status_code": status.HTTP_403_FORBIDDEN,
"description": "Tenant is suspended"
},
ErrorCode.TENANT_QUOTA_EXCEEDED: {
"status_code": status.HTTP_429_TOO_MANY_REQUESTS,
"description": "Tenant quota exceeded"
},
ErrorCode.INVALID_REQUEST: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Invalid request format"
},
ErrorCode.VALIDATION_FAILED: {
"status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
"description": "Request validation failed"
},
ErrorCode.MISSING_REQUIRED_FIELD: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Required field is missing"
},
ErrorCode.INVALID_FIELD_VALUE: {
"status_code": status.HTTP_400_BAD_REQUEST,
"description": "Invalid field value"
},
ErrorCode.SYSTEM_ERROR: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "Internal system error"
},
ErrorCode.SERVICE_UNAVAILABLE: {
"status_code": status.HTTP_503_SERVICE_UNAVAILABLE,
"description": "Service temporarily unavailable"
},
ErrorCode.FILE_LOCK_TIMEOUT: {
"status_code": status.HTTP_423_LOCKED,
"description": "File database lock timeout"
},
ErrorCode.FILE_CORRUPTION: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "File database corruption detected"
},
ErrorCode.ENCRYPTION_FAILED: {
"status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
"description": "Encryption operation failed"
}
}
def raise_api_error(
code: ErrorCode,
message: Optional[str] = None,
**kwargs
) -> None:
"""
Convenience function to raise a standard API error
Args:
code: Error code
message: Optional custom message (uses default if not provided)
**kwargs: Additional error details
Raises:
APIError
"""
error_info = error_responses.get(code, {})
if message is None:
message = error_info.get("description", "An error occurred")
raise APIError(
code=code,
message=message,
status_code=error_info.get("status_code", status.HTTP_400_BAD_REQUEST),
**kwargs
)

View File

@@ -0,0 +1,329 @@
"""
Standard middleware for GT 2.0 CB-REST API
Design principle: Cross-cutting concerns handled simply and consistently
"""
import uuid
import time
import logging
from typing import Optional, Dict, Any, Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from .capability import verify_capability, CapabilityToken, get_verifier
from .response import format_error
from .errors import ErrorCode
logger = logging.getLogger(__name__)
class RequestCorrelationMiddleware(BaseHTTPMiddleware):
"""
Add request correlation ID to all requests for distributed tracing
GT 2.0 Philosophy: Simple observability through request tracking
"""
def __init__(self, app: ASGIApp, header_name: str = "X-Request-ID"):
super().__init__(app)
self.header_name = header_name
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Get or generate request ID
request_id = request.headers.get(self.header_name)
if not request_id:
request_id = str(uuid.uuid4())
# Store in request state for access by handlers
request.state.request_id = request_id
# Process request
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
# Add headers to response
response.headers[self.header_name] = request_id
response.headers["X-Response-Time"] = f"{duration:.3f}s"
# Log request completion
logger.info(
f"Request {request_id} completed",
extra={
"request_id": request_id,
"method": request.method,
"path": request.url.path,
"status": response.status_code,
"duration": duration
}
)
return response
class CapabilityMiddleware(BaseHTTPMiddleware):
"""
Verify capabilities for all protected endpoints
GT 2.0 Philosophy: Security by design, not configuration
"""
def __init__(
self,
app: ASGIApp,
exclude_paths: Optional[list[str]] = None,
audit_logger: Optional[logging.Logger] = None
):
super().__init__(app)
self.exclude_paths = exclude_paths or ["/health", "/ready", "/metrics", "/docs", "/redoc"]
self.audit_logger = audit_logger or logger
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip capability check for excluded paths
if any(request.url.path.startswith(path) for path in self.exclude_paths):
return await call_next(request)
# Extract and verify token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_INVALID.value,
message="Missing or invalid authorization header",
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
try:
# Verify token
token = auth_header.replace("Bearer ", "")
verifier = get_verifier()
capability_token = verifier.verify_token(token)
# Store in request state for access by handlers
request.state.capability_token = capability_token
request.state.tenant_id = capability_token.tenant_id
request.state.user_email = capability_token.sub
# Check signature if provided
signature = request.headers.get("X-Capability-Signature")
if signature and not verifier.verify_signature(token, signature):
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_SIGNATURE_INVALID.value,
message="Invalid capability signature",
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
# Process request
response = await call_next(request)
# Audit log successful capability usage
self.audit_logger.info(
f"Capability used: {request.method} {request.url.path}",
extra={
"request_id": getattr(request.state, "request_id", "unknown"),
"tenant_id": capability_token.tenant_id,
"user": capability_token.sub,
"method": request.method,
"path": request.url.path,
"status": response.status_code
}
)
return response
except Exception as e:
logger.error(f"Capability verification failed: {e}")
return Response(
content=format_error(
code=ErrorCode.CAPABILITY_INVALID.value,
message=str(e),
capability_used="none",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=401,
media_type="application/json"
)
class TenantIsolationMiddleware(BaseHTTPMiddleware):
"""
Ensure perfect tenant isolation for all requests
GT 2.0 Philosophy: Security through architectural design
"""
def __init__(
self,
app: ASGIApp,
enforce_isolation: bool = True
):
super().__init__(app)
self.enforce_isolation = enforce_isolation
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip for non-tenant paths
if not self.enforce_isolation or request.url.path.startswith("/health"):
return await call_next(request)
# Get tenant from capability token (set by CapabilityMiddleware)
capability_token: Optional[CapabilityToken] = getattr(request.state, "capability_token", None)
if not capability_token:
return await call_next(request) # Let other middleware handle auth
# Extract tenant from path if present
path_parts = request.url.path.strip("/").split("/")
path_tenant = None
# Look for tenant ID in common patterns
if "tenants" in path_parts:
idx = path_parts.index("tenants")
if idx + 1 < len(path_parts):
path_tenant = path_parts[idx + 1]
# Check tenant isolation
if path_tenant and capability_token.tenant_id != path_tenant:
# Only super_admin and gt_admin can access other tenants
if capability_token.user_type not in ["super_admin", "gt_admin"]:
logger.warning(
f"Tenant isolation violation attempted",
extra={
"request_id": getattr(request.state, "request_id", "unknown"),
"user_tenant": capability_token.tenant_id,
"requested_tenant": path_tenant,
"user": capability_token.sub,
"path": request.url.path
}
)
return Response(
content=format_error(
code=ErrorCode.TENANT_ISOLATED.value,
message="Cross-tenant access not allowed",
capability_used=f"tenant:{capability_token.tenant_id}:*",
capability_required=f"tenant:{path_tenant}:*",
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=403,
media_type="application/json"
)
# Add tenant context to request
request.state.tenant_context = {
"tenant_id": capability_token.tenant_id,
"requested_tenant": path_tenant,
"user_type": capability_token.user_type,
"isolation_enforced": True
}
return await call_next(request)
class FileSystemIsolationMiddleware(BaseHTTPMiddleware):
"""
Ensure file system access is properly isolated per tenant
GT 2.0 Philosophy: File-based databases with perfect isolation
"""
def __init__(
self,
app: ASGIApp,
base_path: str = "/data"
):
super().__init__(app)
self.base_path = base_path
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Get tenant from request state
tenant_id = getattr(request.state, "tenant_id", None)
if tenant_id:
# Set allowed file paths for this tenant
request.state.allowed_paths = [
f"{self.base_path}/{tenant_id}",
f"{self.base_path}/shared" # Shared read-only resources
]
# Set file encryption key reference
request.state.encryption_key_id = f"{tenant_id}-key-{time.strftime('%Y-%m')}"
return await call_next(request)
class RateLimitMiddleware(BaseHTTPMiddleware):
"""
Simple rate limiting per capability
GT 2.0 Philosophy: Resource protection through simple limits
"""
def __init__(
self,
app: ASGIApp,
requests_per_minute: int = 60,
cache: Optional[Dict[str, list[float]]] = None
):
super().__init__(app)
self.requests_per_minute = requests_per_minute
self.cache = cache or {} # In production, use Redis
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# Skip rate limiting for health checks
if request.url.path.startswith("/health"):
return await call_next(request)
# Get user identifier from capability token
capability_token: Optional[CapabilityToken] = getattr(request.state, "capability_token", None)
if not capability_token:
return await call_next(request)
# Create rate limit key
key = f"{capability_token.tenant_id}:{capability_token.sub}"
# Check rate limit
now = time.time()
minute_ago = now - 60
# Get request times for this key
request_times = self.cache.get(key, [])
# Remove old entries
request_times = [t for t in request_times if t > minute_ago]
# Check if limit exceeded
if len(request_times) >= self.requests_per_minute:
return Response(
content=format_error(
code=ErrorCode.RESOURCE_QUOTA_EXCEEDED.value,
message=f"Rate limit exceeded: {self.requests_per_minute} requests per minute",
capability_used=f"tenant:{capability_token.tenant_id}:*",
details={"retry_after": 60 - (now - request_times[0])},
request_id=getattr(request.state, "request_id", str(uuid.uuid4()))
),
status_code=429,
media_type="application/json",
headers={"Retry-After": str(int(60 - (now - request_times[0])))}
)
# Add current request time
request_times.append(now)
self.cache[key] = request_times
# Add rate limit headers to response
response = await call_next(request)
response.headers["X-RateLimit-Limit"] = str(self.requests_per_minute)
response.headers["X-RateLimit-Remaining"] = str(self.requests_per_minute - len(request_times))
response.headers["X-RateLimit-Reset"] = str(int(minute_ago + 60))
return response

View File

@@ -0,0 +1,161 @@
"""
Standard response formatting for GT 2.0 CB-REST API
Design principle: Simple, consistent, secure responses with built-in audit trail
"""
from typing import Any, Optional, Dict
from datetime import datetime
import uuid
from pydantic import BaseModel, Field
class StandardError(BaseModel):
"""Standard error structure"""
code: str = Field(..., description="Error code (e.g., CAPABILITY_INSUFFICIENT)")
message: str = Field(..., description="Human-readable error message")
capability_required: Optional[str] = Field(None, description="Required capability for this operation")
capability_provided: Optional[str] = Field(None, description="Capability that was provided")
details: Optional[Dict[str, Any]] = Field(None, description="Additional error details")
class StandardResponse(BaseModel):
"""
Standard response format for all CB-REST endpoints
Philosophy: Every response has the same structure, reducing ambiguity
"""
data: Optional[Any] = Field(None, description="Response data (null on error)")
error: Optional[StandardError] = Field(None, description="Error information (null on success)")
capability_used: str = Field(..., description="Capability that authorized this request")
request_id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique request identifier")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
def format_response(
data: Any,
capability_used: str,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format a successful response
Args:
data: The response data
capability_used: The capability that authorized this request
request_id: Optional request ID (will be generated if not provided)
Returns:
Standardized response dictionary
"""
return StandardResponse(
data=data,
error=None,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
).dict(exclude_none=True)
def format_error(
code: str,
message: str,
capability_used: str = "none",
capability_required: Optional[str] = None,
capability_provided: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format an error response
Args:
code: Error code (e.g., CAPABILITY_INSUFFICIENT)
message: Human-readable error message
capability_used: The capability check that failed
capability_required: The required capability
capability_provided: The capability that was provided
details: Additional error details
request_id: Optional request ID
Returns:
Standardized error response dictionary
"""
error = StandardError(
code=code,
message=message,
capability_required=capability_required,
capability_provided=capability_provided,
details=details
)
return StandardResponse(
data=None,
error=error,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
).dict(exclude_none=True)
class BulkOperationResult(BaseModel):
"""Result of a single operation in a bulk request"""
operation_id: str = Field(..., description="Unique identifier for this operation")
action: str = Field(..., description="Action performed (create, update, delete)")
resource_id: Optional[str] = Field(None, description="ID of the affected resource")
success: bool = Field(..., description="Whether the operation succeeded")
error: Optional[StandardError] = Field(None, description="Error if operation failed")
data: Optional[Any] = Field(None, description="Result data if operation succeeded")
class BulkResponse(BaseModel):
"""Response format for bulk operations"""
operations: list[BulkOperationResult] = Field(..., description="Results of all operations")
transaction: bool = Field(..., description="Whether all operations were in a transaction")
total: int = Field(..., description="Total number of operations")
succeeded: int = Field(..., description="Number of successful operations")
failed: int = Field(..., description="Number of failed operations")
capability_used: str = Field(..., description="Capability that authorized this bulk operation")
request_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
def format_bulk_response(
operations: list[BulkOperationResult],
transaction: bool,
capability_used: str,
request_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Format a bulk operation response
Args:
operations: List of operation results
transaction: Whether operations were transactional
capability_used: The capability that authorized this request
request_id: Optional request ID
Returns:
Standardized bulk response dictionary
"""
succeeded = sum(1 for op in operations if op.success)
failed = len(operations) - succeeded
response = BulkResponse(
operations=operations,
transaction=transaction,
total=len(operations),
succeeded=succeeded,
failed=failed,
capability_used=capability_used,
request_id=request_id or str(uuid.uuid4())
)
# For bulk responses, we wrap in standard response
return StandardResponse(
data=response.dict(exclude_none=True),
error=None,
capability_used=capability_used,
request_id=response.request_id
).dict(exclude_none=True)

1
packages/config/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

1
packages/config/dist/index.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,13 @@
{
"name": "@gt2/config",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}

1
packages/types/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

1
packages/types/dist/index.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,13 @@
{
"name": "@gt2/types",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}

254
packages/types/src/index.ts Normal file
View File

@@ -0,0 +1,254 @@
// GT 2.0 Shared TypeScript Types
// Authentication & Authorization
export interface Capability {
resource: string;
actions: string[];
constraints?: {
valid_until?: string;
ip_restrictions?: string[];
usage_limits?: {
max_requests_per_hour?: number;
max_tokens_per_request?: number;
};
};
}
export interface JWTPayload {
sub: string;
tenant_id?: string;
user_type: 'super_admin' | 'tenant_admin' | 'tenant_user';
capabilities: Capability[];
capability_hash: string;
exp: number;
iat: number;
}
export interface User {
id: number;
uuid: string;
email: string;
full_name: string;
user_type: 'super_admin' | 'tenant_admin' | 'tenant_user';
tenant_id?: number;
capabilities: Capability[];
is_active: boolean;
last_login_at?: string;
created_at: string;
updated_at: string;
}
// Tenant Management
export interface Tenant {
id: number;
uuid: string;
name: string;
domain: string;
template: string;
status: 'pending' | 'deploying' | 'active' | 'suspended' | 'terminated';
max_users: number;
resource_limits: {
cpu: string;
memory: string;
storage: string;
max_users?: number;
};
namespace: string;
subdomain: string;
database_path?: string;
created_at: string;
updated_at: string;
}
export interface TenantCreateRequest {
name: string;
domain: string;
template?: string;
max_users?: number;
resource_limits?: {
cpu?: string;
memory?: string;
storage?: string;
};
}
// AI Resources
export interface AIResource {
id: number;
uuid: string;
name: string;
resource_type: 'llm' | 'embedding' | 'image_generation';
provider: string;
model_name: string;
api_endpoint?: string;
configuration: Record<string, any>;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface TenantResource {
id: number;
tenant_id: number;
resource_id: number;
usage_limits: {
max_requests_per_hour: number;
max_tokens_per_request: number;
};
is_enabled: boolean;
created_at: string;
}
// Chat & Conversations
export interface Conversation {
id: number;
title: string;
model_id: string;
system_prompt?: string;
created_by: string;
created_at: string;
}
export interface Message {
id: number;
conversation_id: number;
role: 'user' | 'agent' | 'system';
content: string;
model_used?: string;
tokens_used?: number;
context_sources?: string[];
created_at: string;
}
export interface ChatRequest {
message: string;
conversation_id?: number;
model_id?: string;
system_prompt?: string;
context_sources?: string[];
}
export interface ChatResponse {
message: Message;
conversation: Conversation;
tokens_used: number;
context_sources?: DocumentChunk[];
}
// Document Processing
export interface Document {
id: number;
filename: string;
file_type: string;
processing_status: 'pending' | 'processing' | 'completed' | 'failed';
chunk_count: number;
uploaded_by: string;
created_at: string;
}
export interface DocumentChunk {
id: string;
document_id: number;
content: string;
metadata: Record<string, any>;
embedding?: number[];
similarity_score?: number;
}
export interface DocumentUploadRequest {
file: File | Buffer;
filename: string;
file_type: string;
}
// Usage & Billing
export interface UsageRecord {
id: number;
tenant_id: number;
resource_id: number;
user_email: string;
request_type: string;
tokens_used: number;
cost_cents: number;
metadata: Record<string, any>;
created_at: string;
}
export interface UsageSummary {
tenant_id: number;
period: 'hour' | 'day' | 'month';
total_requests: number;
total_tokens: number;
total_cost_cents: number;
by_resource: Record<string, {
requests: number;
tokens: number;
cost_cents: number;
}>;
}
// API Response Types
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
error?: {
code: string;
message: string;
details?: Record<string, any>;
};
meta?: {
page?: number;
limit?: number;
total?: number;
};
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
meta: {
page: number;
limit: number;
total: number;
total_pages: number;
};
}
// WebSocket Types
export interface WebSocketMessage {
type: 'chat_message' | 'chat_response' | 'typing_start' | 'typing_stop' | 'error';
conversation_id?: number;
data: any;
timestamp: string;
}
// System Configuration
export interface SystemConfig {
tenant_templates: Record<string, {
name: string;
description: string;
resource_limits: {
cpu: string;
memory: string;
storage: string;
};
features: string[];
}>;
ai_providers: Record<string, {
name: string;
api_endpoint: string;
models: string[];
}>;
}
// Audit & Security
export interface AuditLog {
id: number;
user_id?: number;
tenant_id?: number;
action: string;
resource_type?: string;
resource_id?: string;
details: Record<string, any>;
ip_address?: string;
user_agent?: string;
created_at: string;
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}

1
packages/ui/dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export {};

1
packages/ui/dist/index.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

13
packages/ui/package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "@gt2/ui",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}

View File

@@ -0,0 +1 @@
export {};

View 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(['*']);
});
});
});

View File

@@ -0,0 +1 @@
export {};

View 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);
}
});
});
});

View File

@@ -0,0 +1,3 @@
/**
* Test setup for utility functions
*/

20
packages/utils/dist/__tests__/setup.js vendored Normal file
View 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);

View File

@@ -0,0 +1 @@
export {};

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
export {};

1
packages/utils/dist/index.js vendored Normal file
View File

@@ -0,0 +1 @@
export {};

58
packages/utils/dist/tenant.d.ts vendored Normal file
View 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
View 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
View 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
View 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
};
}

View File

@@ -0,0 +1,33 @@
/** @type {import('jest').Config} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'<rootDir>/src/**/*.{test,spec}.{js,jsx,ts,tsx}',
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,jsx,ts,tsx}',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapping: {
'^@gt2/types$': '<rootDir>/../types/src/index.ts',
},
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
};

View File

@@ -0,0 +1,13 @@
{
"name": "@gt2/utils",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
}

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
};
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}