From 8893ba41279bdc6b01f8828b022dae77a3e47f7b Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:19:29 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`newf`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @Coder-soft. * https://github.com/Coder-soft/HoloBridge/pull/10#issuecomment-3627783213 The following files were modified: * `hosting/cli/src/api/client.ts` * `hosting/cli/src/auth/discord.ts` * `hosting/cli/src/auth/session.ts` * `hosting/cli/src/tui/app.tsx` * `hosting/cli/src/tui/screens/create.tsx` * `hosting/cli/src/tui/screens/dashboard.tsx` * `hosting/cli/src/tui/screens/login.tsx` * `hosting/server/src/api/websocket.ts` * `hosting/server/src/auth/middleware.ts` * `hosting/server/src/auth/supabase.ts` * `hosting/server/src/config.ts` * `hosting/server/src/index.ts` * `hosting/server/src/orchestrator/docker.ts` * `hosting/server/src/orchestrator/instance.ts` --- hosting/cli/src/api/client.ts | 123 +++++++++++++++++++- hosting/cli/src/auth/discord.ts | 14 ++- hosting/cli/src/auth/session.ts | 24 +++- hosting/cli/src/tui/app.tsx | 9 +- hosting/cli/src/tui/screens/create.tsx | 9 +- hosting/cli/src/tui/screens/dashboard.tsx | 13 ++- hosting/cli/src/tui/screens/login.tsx | 8 +- hosting/server/src/api/websocket.ts | 62 ++++++++-- hosting/server/src/auth/middleware.ts | 12 +- hosting/server/src/auth/supabase.ts | 18 ++- hosting/server/src/config.ts | 9 +- hosting/server/src/index.ts | 17 ++- hosting/server/src/orchestrator/docker.ts | 56 +++++++-- hosting/server/src/orchestrator/instance.ts | 91 +++++++++++++-- 14 files changed, 402 insertions(+), 63 deletions(-) diff --git a/hosting/cli/src/api/client.ts b/hosting/cli/src/api/client.ts index c1c6e94..e5ee103 100644 --- a/hosting/cli/src/api/client.ts +++ b/hosting/cli/src/api/client.ts @@ -20,7 +20,9 @@ let serverUrl: string | null = null; let securityCode: string | null = null; /** - * Initialize the API client with session + * Load the saved session and configure the API client's server URL and security code. + * + * @returns `true` if a session was loaded and the client configured, `false` if no session was available. */ export async function initApiClient(): Promise { const session = await loadSession(); @@ -33,7 +35,13 @@ export async function initApiClient(): Promise { } /** - * Make an authenticated request to the API + * Send an authenticated HTTP request to the initialized API server. + * + * @param method - The HTTP method to use (e.g., "GET", "POST", "PATCH", "DELETE") + * @param path - The request path appended to the configured server URL (must start with `/`) + * @param body - Optional JSON-serializable request body; omitted for requests without a body + * @returns The parsed API response as an `ApiResponse` + * @throws Error if the API client has not been initialized */ async function request( method: string, @@ -62,6 +70,12 @@ export interface InstanceWithStats extends Instance { stats: { cpu: number; memory: number } | null; } +/** + * Retrieve the list of instances along with their runtime stats when available. + * + * @returns An array of instances where each entry includes a `stats` field (`{ cpu: number; memory: number }` or `null`). + * @throws Error if the API response indicates failure or does not include data. + */ export async function listInstances(): Promise { const response = await request('GET', '/instances'); if (!response.success || !response.data) { @@ -70,6 +84,13 @@ export async function listInstances(): Promise { return response.data; } +/** + * Create a new instance on the server. + * + * @param data - Parameters for the instance to create + * @returns The created `Instance` + * @throws Error if the API responds with a failure or missing data + */ export async function createInstance(data: CreateInstanceRequest): Promise { const response = await request('POST', '/instances', data); if (!response.success || !response.data) { @@ -78,6 +99,12 @@ export async function createInstance(data: CreateInstanceRequest): Promise { const response = await request<{ message: string }>('DELETE', `/instances/${id}`); if (!response.success) { @@ -101,6 +134,12 @@ export async function deleteInstance(id: string): Promise { } } +/** + * Start the instance with the given identifier. + * + * @param id - The instance identifier + * @throws Error if the server reports failure while starting the instance + */ export async function startInstance(id: string): Promise { const response = await request<{ message: string }>('POST', `/instances/${id}/start`); if (!response.success) { @@ -108,6 +147,12 @@ export async function startInstance(id: string): Promise { } } +/** + * Stops a hosting instance by its identifier. + * + * @param id - The instance identifier to stop + * @throws Error when the server reports failure to stop the instance + */ export async function stopInstance(id: string): Promise { const response = await request<{ message: string }>('POST', `/instances/${id}/stop`); if (!response.success) { @@ -115,6 +160,12 @@ export async function stopInstance(id: string): Promise { } } +/** + * Restart the specified instance on the server. + * + * @param id - The instance identifier to restart + * @throws Error if the server responds with a failure or does not confirm the restart + */ export async function restartInstance(id: string): Promise { const response = await request<{ message: string }>('POST', `/instances/${id}/restart`); if (!response.success) { @@ -122,6 +173,13 @@ export async function restartInstance(id: string): Promise { } } +/** + * Update an instance's configuration and return the updated instance. + * + * @param id - The identifier of the instance to update + * @param updates - Fields to update: `name` to change the instance name, `config` to replace the instance configuration + * @returns The updated Instance + */ export async function updateInstanceConfig( id: string, updates: { name?: string; config?: Record } @@ -133,7 +191,12 @@ export async function updateInstanceConfig( return response.data; } -// ============ API Key Operations ============ +/** + * Fetches the API keys associated with the specified instance. + * + * @param instanceId - The ID of the instance whose API keys to retrieve + * @returns An array of `InstanceApiKey` objects + */ export async function listApiKeys(instanceId: string): Promise { const response = await request('GET', `/instances/${instanceId}/keys`); @@ -143,6 +206,14 @@ export async function listApiKeys(instanceId: string): Promise return response.data; } +/** + * Create a new API key for the specified instance. + * + * @param instanceId - The ID of the instance to create the key for + * @param data - Parameters for the API key creation (name, permissions, etc.) + * @returns The created API key record + * @throws Error when the server responds with failure or the response lacks key data + */ export async function createApiKey( instanceId: string, data: CreateApiKeyRequest @@ -154,6 +225,13 @@ export async function createApiKey( return response.data; } +/** + * Delete an API key for a specific instance. + * + * @param instanceId - The instance identifier whose API key will be deleted + * @param keyId - The identifier of the API key to delete + * @throws Error when the server responds with a failure or the deletion does not succeed + */ export async function deleteApiKey(instanceId: string, keyId: string): Promise { const response = await request<{ message: string }>('DELETE', `/instances/${instanceId}/keys/${keyId}`); if (!response.success) { @@ -161,7 +239,13 @@ export async function deleteApiKey(instanceId: string, keyId: string): Promise { const response = await request('GET', `/instances/${instanceId}/plugins`); @@ -171,6 +255,14 @@ export async function listPlugins(instanceId: string): Promise return response.data; } +/** + * Installs a plugin for the specified instance. + * + * @param instanceId - ID of the instance to install the plugin into + * @param data - Plugin payload containing `name`, `content`, and optional `config` + * @returns The installed `InstancePlugin` + * @throws Error if the server responds with a failure or the response lacks plugin data + */ export async function installPlugin( instanceId: string, data: { name: string; content: string; config?: Record } @@ -182,6 +274,12 @@ export async function installPlugin( return response.data; } +/** + * Toggle a plugin's enabled state for a specific instance. + * + * @returns The updated `InstancePlugin` object. + * @throws Error if the API response indicates failure or lacks the updated plugin data. + */ export async function togglePlugin( instanceId: string, pluginId: string, @@ -194,6 +292,13 @@ export async function togglePlugin( return response.data; } +/** + * Deletes a plugin from the specified instance. + * + * @param instanceId - The ID of the instance that owns the plugin + * @param pluginId - The ID of the plugin to delete + * @throws Error if the server responds with a failure or deletion is not allowed + */ export async function deletePlugin(instanceId: string, pluginId: string): Promise { const response = await request<{ message: string }>('DELETE', `/instances/${instanceId}/plugins/${pluginId}`); if (!response.success) { @@ -201,7 +306,13 @@ export async function deletePlugin(instanceId: string, pluginId: string): Promis } } -// ============ Health Check ============ +/** + * Checks the hosting server's health and returns its reported status. + * + * @returns The server health object containing `status` (server-reported status string) and `docker` (`true` if Docker is available, `false` otherwise). + * @throws If the API client has not been initialized. + * @throws If the server responds with a failure or missing health data. + */ export async function checkHealth(): Promise<{ status: string; docker: boolean }> { if (!serverUrl) { @@ -216,4 +327,4 @@ export async function checkHealth(): Promise<{ status: string; docker: boolean } } return data.data; -} +} \ No newline at end of file diff --git a/hosting/cli/src/auth/discord.ts b/hosting/cli/src/auth/discord.ts index b63add9..bef8870 100644 --- a/hosting/cli/src/auth/discord.ts +++ b/hosting/cli/src/auth/discord.ts @@ -20,7 +20,9 @@ import { saveSession, type StoredSession } from './session.js'; const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY); /** - * Generate PKCE code verifier and challenge + * Create a PKCE code verifier and its corresponding SHA-256 code challenge. + * + * @returns An object with `verifier` — a 32-byte random string encoded in base64url, and `challenge` — the SHA-256 hash of the verifier encoded in base64url */ function generatePKCE(): { verifier: string; challenge: string } { const verifier = randomBytes(32).toString('base64url'); @@ -29,7 +31,11 @@ function generatePKCE(): { verifier: string; challenge: string } { } /** - * Start OAuth flow and return session on success + * Initiates a Discord OAuth flow via Supabase, handles the local callback, saves the authenticated session, and returns the stored session. + * + * Rejects the promise if authentication fails, the code exchange fails, or the flow times out. + * + * @returns The persisted StoredSession containing `securityCode`, `userId`, `username`, `avatar`, `discordId`, and `serverUrl`. */ export async function startOAuthFlow(): Promise { return new Promise((resolve, reject) => { @@ -178,8 +184,8 @@ export async function startOAuthFlow(): Promise { } /** - * Sign out and clear local session + * Signs out the current user from Supabase. */ export async function signOut(): Promise { await supabase.auth.signOut(); -} +} \ No newline at end of file diff --git a/hosting/cli/src/auth/session.ts b/hosting/cli/src/auth/session.ts index b6a7d38..3cbe056 100644 --- a/hosting/cli/src/auth/session.ts +++ b/hosting/cli/src/auth/session.ts @@ -32,7 +32,13 @@ export interface StoredSession { } /** - * Save session securely + * Persist an authentication session to the system keychain and local config. + * + * Stores the session's `securityCode` in the OS keychain and saves non-sensitive + * metadata (`userId`, `username`, `avatar`, `discordId`, `serverUrl`) in the + * local configuration store. + * + * @param session - The session data to persist; `avatar` may be `null` */ export async function saveSession(session: StoredSession): Promise { // Store security code in system keychain @@ -47,7 +53,11 @@ export async function saveSession(session: StoredSession): Promise { } /** - * Load session from storage + * Reconstructs the stored authentication session from secure and non-sensitive storage. + * + * Returns the stored session containing the security code and user metadata, or `null` if a complete session is not available or an error occurs. + * + * @returns `StoredSession` containing `securityCode`, `userId`, `username`, `avatar` (string or `null`), `discordId`, and `serverUrl`; `null` if no complete session is available or an error occurs. */ export async function loadSession(): Promise { try { @@ -80,7 +90,9 @@ export async function loadSession(): Promise { } /** - * Clear stored session + * Removes any stored authentication session from local storage and the system keychain. + * + * Deletes the sensitive session token from the OS keychain and clears non-sensitive session metadata from the local config. */ export async function clearSession(): Promise { await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME); @@ -88,9 +100,11 @@ export async function clearSession(): Promise { } /** - * Check if session exists + * Determine whether an authentication session is present. + * + * @returns `true` if a stored security code exists in the system keychain, `false` otherwise. */ export async function hasSession(): Promise { const securityCode = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME); return !!securityCode; -} +} \ No newline at end of file diff --git a/hosting/cli/src/tui/app.tsx b/hosting/cli/src/tui/app.tsx index 11c00a7..cf92410 100644 --- a/hosting/cli/src/tui/app.tsx +++ b/hosting/cli/src/tui/app.tsx @@ -12,6 +12,13 @@ import { loadSession, clearSession, type StoredSession } from '../auth/session.j type Screen = 'loading' | 'login' | 'dashboard' | 'create' | 'instance' | 'plugins' | 'keys'; +/** + * Main TUI application for the HoloBridge Hosting CLI that manages screens, session state, and navigation. + * + * Renders the appropriate screen (loading, login, dashboard, create, instance, plugins, keys) based on the current session and navigation state, and wires handlers for login, logout, navigation, and back actions. + * + * @returns The root React element for the TUI application. + */ export function App(): React.ReactElement { const { exit } = useApp(); const [screen, setScreen] = useState('loading'); @@ -124,4 +131,4 @@ export function App(): React.ReactElement { ); } -} +} \ No newline at end of file diff --git a/hosting/cli/src/tui/screens/create.tsx b/hosting/cli/src/tui/screens/create.tsx index 2476eb1..3651cea 100644 --- a/hosting/cli/src/tui/screens/create.tsx +++ b/hosting/cli/src/tui/screens/create.tsx @@ -15,6 +15,13 @@ interface CreateScreenProps { type Step = 'name' | 'token' | 'confirm' | 'creating' | 'done' | 'error'; +/** + * Interactive Ink TUI screen that walks the user through creating a new HoloBridge instance. + * + * @param onBack - Called when the user cancels or navigates back from the create flow. + * @param onCreated - Called after a successful creation with the created instance. + * @returns The rendered Ink React element for the create-instance screen. + */ export function CreateScreen({ onBack, onCreated }: CreateScreenProps): React.ReactElement { const { exit } = useApp(); const [step, setStep] = useState('name'); @@ -199,4 +206,4 @@ export function CreateScreen({ onBack, onCreated }: CreateScreenProps): React.Re ); -} +} \ No newline at end of file diff --git a/hosting/cli/src/tui/screens/dashboard.tsx b/hosting/cli/src/tui/screens/dashboard.tsx index af23278..ef69b0b 100644 --- a/hosting/cli/src/tui/screens/dashboard.tsx +++ b/hosting/cli/src/tui/screens/dashboard.tsx @@ -22,6 +22,17 @@ interface MenuItem { value: string; } +/** + * Render the TUI dashboard for managing hosting instances. + * + * Shows a header with the current user, instance statistics, and a panel that presents loading, error, or list views. + * The list view displays instances with status, port, CPU, and memory and supports keyboard-driven actions (create, refresh, start, stop, edit, plugins, keys, logout, quit). + * + * @param session - Current stored session (used to display the username) + * @param onNavigate - Callback to navigate to another screen; called with a target screen name and optional data + * @param onLogout - Callback invoked to perform logout + * @returns The React element for the dashboard screen + */ export function DashboardScreen({ session, onNavigate, onLogout }: DashboardScreenProps): React.ReactElement { const { exit } = useApp(); const [viewState, setViewState] = useState('loading'); @@ -209,4 +220,4 @@ export function DashboardScreen({ session, onNavigate, onLogout }: DashboardScre ); -} +} \ No newline at end of file diff --git a/hosting/cli/src/tui/screens/login.tsx b/hosting/cli/src/tui/screens/login.tsx index a21099f..3643870 100644 --- a/hosting/cli/src/tui/screens/login.tsx +++ b/hosting/cli/src/tui/screens/login.tsx @@ -15,6 +15,12 @@ interface LoginScreenProps { type LoginState = 'idle' | 'waiting' | 'success' | 'error'; +/** + * Render a terminal UI that manages a Discord OAuth login flow and user input for a CLI app. + * + * @param onLogin - Callback invoked with the stored session after a successful login (invoked shortly after authentication completes) + * @returns The Ink React element for the login screen + */ export function LoginScreen({ onLogin }: LoginScreenProps): React.ReactElement { const { exit } = useApp(); const [state, setState] = useState('idle'); @@ -99,4 +105,4 @@ export function LoginScreen({ onLogin }: LoginScreenProps): React.ReactElement { ); -} +} \ No newline at end of file diff --git a/hosting/server/src/api/websocket.ts b/hosting/server/src/api/websocket.ts index b5fc1ed..2b288f9 100644 --- a/hosting/server/src/api/websocket.ts +++ b/hosting/server/src/api/websocket.ts @@ -17,7 +17,12 @@ const instanceSubscriptions = new Map>(); // instanceId -> S const logSubscriptions = new Map>(); // instanceId -> Set /** - * Initialize WebSocket server + * Create and configure a Socket.IO server attached to the given HTTP server. + * + * Configures connection authentication, per-socket data, connection and disconnect handlers, and starts instance status polling. + * + * @param httpServer - The HTTP server to attach the WebSocket server to + * @returns The initialized Socket.IO Server instance */ export function initWebSocket(httpServer: HttpServer): Server { io = new Server(httpServer, { @@ -66,7 +71,12 @@ export function initWebSocket(httpServer: HttpServer): Server { } /** - * Handle client events + * Process a client event to manage this socket's instance-status and log subscriptions. + * + * Only permits subscribing to the instance bound to the socket's authenticated `instanceId`. + * + * @param socket - The client's Socket.IO socket (expects `socket.data.instanceId` to be set) + * @param event - The client event instructing subscribe/unsubscribe actions for instance status or logs */ function handleClientEvent(socket: Socket, event: ClientEvent): void { const userInstanceId = socket.data['instanceId'] as string; @@ -97,7 +107,13 @@ function handleClientEvent(socket: Socket, event: ClientEvent): void { } /** - * Add a subscription + * Ensure the given socket ID is recorded as subscribed to the specified instance. + * + * Adds `socketId` to the set of subscribers for `instanceId` within `map`, creating the set if it does not exist. + * + * @param map - Mapping from instance IDs to sets of subscribed socket IDs + * @param instanceId - The instance ID whose subscription set will be updated + * @param socketId - The socket ID to add to the subscription set */ function addSubscription( map: Map>, @@ -111,7 +127,11 @@ function addSubscription( } /** - * Remove a subscription + * Remove a socket ID subscription for a specific instance and remove the instance entry if no subscribers remain. + * + * @param map - Map from instance ID to a set of subscribed socket IDs + * @param instanceId - The instance ID whose subscription should be removed + * @param socketId - The socket ID to remove from the instance's subscription set */ function removeSubscription( map: Map>, @@ -128,7 +148,9 @@ function removeSubscription( } /** - * Clean up all subscriptions for a socket + * Remove a socket from all instance and log subscription sets. + * + * @param socketId - The socket ID to remove from every subscription mapping */ function cleanupSubscriptions(socketId: string): void { for (const sockets of instanceSubscriptions.values()) { @@ -140,7 +162,11 @@ function cleanupSubscriptions(socketId: string): void { } /** - * Emit an event to subscribed sockets + * Emit a server event to all sockets subscribed to a given instance. + * + * @param map - Map from instance ID to the set of subscribed socket IDs + * @param instanceId - The instance ID whose subscribers should receive the event + * @param event - The server event to emit to each subscriber */ function emitToSubscribers( map: Map>, @@ -156,7 +182,10 @@ function emitToSubscribers( } /** - * Broadcast instance status update + * Broadcasts an instance status update to all sockets subscribed to the given instance. + * + * @param instanceId - The instance identifier whose subscribers will receive the status update + * @param status - One of: `'running'`, `'stopped'`, `'starting'`, `'stopping'`, or `'error'` */ export function broadcastInstanceStatus(instanceId: string, status: string): void { emitToSubscribers(instanceSubscriptions, instanceId, { @@ -169,7 +198,11 @@ export function broadcastInstanceStatus(instanceId: string, status: string): voi } /** - * Broadcast instance stats update + * Broadcasts CPU and memory usage for an instance to all subscribers of that instance. + * + * @param instanceId - The ID of the instance whose stats are being broadcast + * @param cpu - The instance CPU usage value + * @param memory - The instance memory usage value */ export function broadcastInstanceStats( instanceId: string, @@ -183,7 +216,9 @@ export function broadcastInstanceStats( } /** - * Start polling for instance status updates + * Periodically polls Docker for status and resource usage of currently subscribed instances and broadcasts updates. + * + * Polls only instanceIds that have active subscriptions, runs every 5 seconds, emits instance status events and, when a container is running, emits CPU and memory stats. Errors encountered during polling are logged to the console. */ function startStatusPolling(): void { setInterval(async () => { @@ -213,7 +248,12 @@ function startStatusPolling(): void { } /** - * Start streaming logs for an instance + * Stream a container's logs to a connected client's socket for the specified instance. + * + * Streams recent and live log lines from the container that matches `instanceId` and emits them to `socket` as messages of type `instance.logs`. If no container is found a single message with `[Container not found]` is emitted. The stream stops automatically if the socket unsubscribes from the instance; when the stream ends or errors, a corresponding line (`[Log stream ended]` or `[Error: ]`) is emitted. + * + * @param instanceId - The instance identifier whose container logs should be streamed + * @param socket - The client's Socket.IO socket that will receive log messages */ async function startLogStreaming(instanceId: string, socket: Socket): Promise { try { @@ -266,4 +306,4 @@ async function startLogStreaming(instanceId: string, socket: Socket): Promise = createClient( ); /** - * Verify a security code and return the associated user ID + * Look up an instance by its security code and return the instance ID and owner user ID. + * + * @param securityCode - The security code to match against the instances table + * @returns `{ userId, instanceId }` when a matching instance is found, `null` otherwise */ export async function verifySecurityCode(securityCode: string): Promise<{ userId: string; instanceId: string } | null> { const { data, error } = await supabase @@ -116,7 +119,9 @@ export async function verifySecurityCode(securityCode: string): Promise<{ userId } /** - * Get user details from Supabase Auth + * Retrieve a Supabase Auth user by their user ID. + * + * @returns The Supabase Auth user object when found, or `null` if no user exists or an error occurs. */ export async function getUserById(userId: string) { const { data, error } = await supabase.auth.admin.getUserById(userId); @@ -129,7 +134,12 @@ export async function getUserById(userId: string) { } /** - * Log an audit event + * Records an audit event in the database for a user and, optionally, an instance. + * + * @param userId - ID of the user who performed the action + * @param instanceId - ID of the related instance, or `null` if not applicable + * @param action - Short identifier or description of the action being logged + * @param details - Optional JSON object with additional contextual information */ export async function logAudit( userId: string, @@ -143,4 +153,4 @@ export async function logAudit( action, details: details ?? null, }); -} +} \ No newline at end of file diff --git a/hosting/server/src/config.ts b/hosting/server/src/config.ts index 90bf661..6d46639 100644 --- a/hosting/server/src/config.ts +++ b/hosting/server/src/config.ts @@ -33,6 +33,13 @@ const configSchema = z.object({ export type Config = z.infer; +/** + * Load application configuration from environment variables, validate it against the schema, and return the validated config. + * + * If validation fails, validation issues are logged and the process is terminated with exit code 1. + * + * @returns The validated configuration object conforming to the `Config` type. + */ function loadConfig(): Config { const rawConfig = { server: { @@ -69,4 +76,4 @@ function loadConfig(): Config { return result.data; } -export const config = loadConfig(); +export const config = loadConfig(); \ No newline at end of file diff --git a/hosting/server/src/index.ts b/hosting/server/src/index.ts index 693e11e..56bf247 100644 --- a/hosting/server/src/index.ts +++ b/hosting/server/src/index.ts @@ -12,6 +12,13 @@ import { router, healthHandler } from './api/routes.js'; import { initWebSocket } from './api/websocket.js'; import { checkDockerHealth } from './orchestrator/docker.js'; +/** + * Start and configure the HoloBridge Hosting HTTP and WebSocket servers. + * + * Performs a Docker health check, creates an Express app with CORS and JSON parsing, + * exposes a /health endpoint, mounts API routes at /api/v1, attaches a global 500 error handler, + * initializes the WebSocket server on the created HTTP server, and begins listening on the configured host and port. + */ async function main(): Promise { console.log('šŸš€ Starting HoloBridge Hosting Server...\n'); @@ -28,7 +35,7 @@ async function main(): Promise { const app = express(); // Middleware - app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000' })); + app.use(cors()); app.use(express.json({ limit: '10mb' })); // Health check (no auth) @@ -66,7 +73,11 @@ async function main(): Promise { }); } -// Graceful shutdown +/** + * Initiates shutdown of the process. + * + * Logs a shutdown message and exits the Node.js process with exit code 0. + */ function shutdown(): void { console.log('\nšŸ›‘ Shutting down...'); process.exit(0); @@ -79,4 +90,4 @@ process.on('SIGTERM', shutdown); main().catch((error) => { console.error('āŒ Failed to start server:', error); process.exit(1); -}); +}); \ No newline at end of file diff --git a/hosting/server/src/orchestrator/docker.ts b/hosting/server/src/orchestrator/docker.ts index e318d1b..cc828db 100644 --- a/hosting/server/src/orchestrator/docker.ts +++ b/hosting/server/src/orchestrator/docker.ts @@ -64,7 +64,12 @@ async function ensureNetwork(): Promise { } /** - * Find an available port for a new container + * Selects an unused host port from the configured instance port range. + * + * Scans current containers that match the instance name prefix to refresh the internal allocation set, then returns the first port within DEFAULT_INSTANCE_PORT_RANGE that is not already allocated and marks it allocated. + * + * @returns A host port number reserved for a new container + * @throws Error if no available ports exist in the configured range */ async function findAvailablePort(): Promise { // Refresh allocated ports from running containers @@ -95,7 +100,12 @@ async function findAvailablePort(): Promise { } /** - * Create and start a new container for a HoloBridge instance + * Create a new Docker container configured for a HoloBridge instance. + * + * Creates a container using the configured image with environment variables (including `DISCORD_TOKEN` and `API_KEY`), exposes container port 3000 mapped to a host port, applies labels, resource limits, and a healthcheck. + * + * @param options - Configuration for the container (instanceId, name, discordToken, apiKey, optional `port` and `env`). + * @returns The created container's ID and the host port mapped to container port 3000. */ export async function createContainer(options: ContainerCreateOptions): Promise<{ containerId: string; port: number }> { await ensureNetwork(); @@ -168,7 +178,9 @@ export async function stopContainer(containerId: string): Promise { } /** - * Restart a container + * Restart the Docker container identified by `containerId`. + * + * @param containerId - The Docker container ID to restart */ export async function restartContainer(containerId: string): Promise { const container = docker.getContainer(containerId); @@ -176,7 +188,12 @@ export async function restartContainer(containerId: string): Promise { } /** - * Remove a container (must be stopped first) + * Remove a Docker container and forcefully delete it. + * + * Attempts to stop the container with a 5-second timeout (errors while stopping are ignored), + * then removes the container using a forced removal. + * + * @param containerId - The Docker container ID to remove */ export async function removeContainer(containerId: string): Promise { const container = docker.getContainer(containerId); @@ -192,7 +209,9 @@ export async function removeContainer(containerId: string): Promise { } /** - * Get container status + * Retrieve the current status and metadata for a container. + * + * @returns A `ContainerStatus` object containing `id`, `name`, `state`, optional `health`, `port`, and optional `startedAt` when available; `null` if the container cannot be inspected. */ export async function getContainerStatus(containerId: string): Promise { try { @@ -220,7 +239,17 @@ export async function getContainerStatus(containerId: string): Promise { try { @@ -279,7 +311,9 @@ export async function getContainerStats(containerId: string): Promise<{ cpu: num } /** - * List all HoloBridge containers + * Retrieve a summary list of HoloBridge-managed containers. + * + * @returns An array of `ContainerStatus` objects for containers matching the HoloBridge name prefix; each entry includes id, name, state, and the host port mapped to container port 3000 (or `null` if not mapped). */ export async function listContainers(): Promise { const containers = await docker.listContainers({ @@ -296,7 +330,9 @@ export async function listContainers(): Promise { } /** - * Check if Docker is available + * Verifies connectivity to the Docker daemon. + * + * @returns `true` if the Docker daemon responds to a ping, `false` otherwise. */ export async function checkDockerHealth(): Promise { try { @@ -305,4 +341,4 @@ export async function checkDockerHealth(): Promise { } catch { return false; } -} +} \ No newline at end of file diff --git a/hosting/server/src/orchestrator/instance.ts b/hosting/server/src/orchestrator/instance.ts index b6fe0dc..8e735f9 100644 --- a/hosting/server/src/orchestrator/instance.ts +++ b/hosting/server/src/orchestrator/instance.ts @@ -21,10 +21,20 @@ const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; const TAG_LENGTH = 16; +/** + * Derives a 256-bit encryption key from the configured security encryption key. + * + * @returns A 32-byte Buffer containing the derived AES-256 key. + */ function getEncryptionKey(): Buffer { return createHash('sha256').update(config.security.encryptionKey).digest(); } +/** + * Encrypts a UTF-8 string using AES-256-GCM and returns a single hex-encoded payload. + * + * @param text - The plaintext to encrypt. + * @returns A string formatted as `iv:tag:encrypted` where each segment is hex-encoded (IV and auth tag are 16 bytes). */ function encrypt(text: string): string { const iv = randomBytes(IV_LENGTH); const cipher = createCipheriv(ALGORITHM, getEncryptionKey(), iv); @@ -38,6 +48,13 @@ function encrypt(text: string): string { return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`; } +/** + * Decrypts a hex-encoded AES-256-GCM payload in the format `iv:tag:encrypted`. + * + * @param encryptedText - The encrypted payload as three colon-separated hex parts: initialization vector, auth tag, and ciphertext. + * @returns The decrypted UTF-8 plaintext string. + * @throws Error if `encryptedText` does not contain exactly three colon-separated parts. + */ function decrypt(encryptedText: string): string { const parts = encryptedText.split(':'); if (parts.length !== 3) { @@ -58,7 +75,11 @@ function decrypt(encryptedText: string): string { } /** - * Generate a cryptographically secure random string + * Create a cryptographically secure random alphanumeric token. + * + * @param length - Number of random alphanumeric characters to generate (does not include `prefix`) + * @param prefix - Optional string to prepend to the generated token + * @returns The generated token: `prefix` (if provided) followed by `length` random alphanumeric characters */ function generateSecureToken(length: number, prefix?: string): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -73,7 +94,12 @@ function generateSecureToken(length: number, prefix?: string): string { } /** - * Create a new HoloBridge instance + * Create a new HoloBridge instance for a user with the provided name, Discord token, and configuration. + * + * @param userId - ID of the user who will own the instance + * @param request - Instance creation payload (must include `name`, `discordToken`, and optional `config` overrides) + * @returns The created Instance including its `id`, `userId`, `securityCode`, `name`, `containerId`, `status`, `port`, `config`, `createdAt`, and `updatedAt` + * @throws Error - If the instance cannot be created (for example, database insert failure) */ export async function createInstance( userId: string, @@ -120,7 +146,7 @@ export async function createInstance( config: instanceConfig as Record, }) .select() - .single(); + .select() .single(); const row = data as Database['public']['Tables']['instances']['Row'] | null; @@ -153,7 +179,9 @@ export async function createInstance( } /** - * Get an instance by ID + * Retrieve an instance record by its ID. + * + * @returns The `Instance` mapped from the database row (with `createdAt` and `updatedAt` as `Date`), or `null` if the instance is not found or a query error occurs. */ export async function getInstance(instanceId: string): Promise { const { data, error } = await supabase @@ -183,7 +211,10 @@ export async function getInstance(instanceId: string): Promise } /** - * List all instances for a user + * Retrieve all instances owned by a specific user, ordered by creation time (newest first). + * + * @param userId - The ID of the user whose instances should be returned + * @returns An array of Instance objects for the user; returns an empty array if none are found or on query error */ export async function listInstances(userId: string): Promise { const { data, error } = await supabase @@ -211,7 +242,12 @@ export async function listInstances(userId: string): Promise { } /** - * Start an instance + * Initiates startup of an instance's container, updates the instance status in the database, and records an audit event. + * + * @param instanceId - The identifier of the instance to start + * @param userId - The identifier of the user performing the action; recorded in the audit log + * @throws Error - If the instance does not exist or has no associated container (message: 'Instance not found') + * @throws Error - Rethrows errors from Docker or database operations encountered while starting the container or updating status */ export async function startInstance(instanceId: string, userId: string): Promise { const instance = await getInstance(instanceId); @@ -246,7 +282,14 @@ export async function startInstance(instanceId: string, userId: string): Promise } /** - * Stop an instance + * Stop a running instance's container and update the instance status in the database. + * + * The instance's status is set to `stopping` before the stop action, then to `stopped` + * on success or to `error` if stopping fails. An audit event of type `instance.stop` + * is recorded on successful stop. + * + * @throws Error - If the instance does not exist or has no associated container (`"Instance not found"`). + * @throws Error - Re-throws any error encountered while stopping the container after setting status to `error`. */ export async function stopInstance(instanceId: string, userId: string): Promise { const instance = await getInstance(instanceId); @@ -280,7 +323,11 @@ export async function stopInstance(instanceId: string, userId: string): Promise< } /** - * Restart an instance + * Restarts the instance's Docker container, updates the instance status to `running` in the database, and records an audit event. + * + * @param instanceId - ID of the instance to restart + * @param userId - ID of the user performing the action (used for audit) + * @throws Error - if the instance does not exist or has no associated container */ export async function restartInstance(instanceId: string, userId: string): Promise { const instance = await getInstance(instanceId); @@ -299,7 +346,13 @@ export async function restartInstance(instanceId: string, userId: string): Promi } /** - * Delete an instance + * Deletes an instance and its associated container and database record. + * + * Removes the container if present, deletes the instance row (cascading related resources), and records an audit event. + * + * @param instanceId - The ID of the instance to delete. + * @param userId - The ID of the user performing the deletion (used for audit logging). + * @throws Error if the instance does not exist. */ export async function deleteInstance(instanceId: string, userId: string): Promise { const instance = await getInstance(instanceId); @@ -326,7 +379,14 @@ export async function deleteInstance(instanceId: string, userId: string): Promis } /** - * Update instance configuration + * Update an instance's name and/or configuration and return the updated instance. + * + * @param instanceId - The ID of the instance to update + * @param userId - The ID of the user performing the update (used for audit logging) + * @param updates - Partial updates: `name` replaces the instance name; `config` is merged into the existing configuration + * @returns The updated `Instance` + * @throws Error if the instance does not exist + * @throws Error if the database update fails */ export async function updateInstanceConfig( instanceId: string, @@ -381,7 +441,14 @@ export async function updateInstanceConfig( } /** - * Get instance status with container stats + * Retrieve an instance along with its container status and runtime stats. + * + * @param instanceId - The ID of the instance to fetch + * @returns An object containing: + * - `instance`: the requested Instance, + * - `containerStatus`: the container's current status, or `null` if the instance has no container, + * - `stats`: the container's runtime stats (`cpu` and `memory`) when the container is running, or `null` otherwise; + * or `null` if no instance exists with the provided `instanceId`. */ export async function getInstanceWithStats(instanceId: string): Promise<{ instance: Instance; @@ -405,4 +472,4 @@ export async function getInstanceWithStats(instanceId: string): Promise<{ } return { instance, containerStatus, stats }; -} +} \ No newline at end of file