UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

227 lines • 7.98 kB
/** * Server Utilities for NeuroLink CLI * Shared utility functions for server management commands (serve.ts and server.ts) */ import { timingSafeEqual } from "crypto"; import fs from "fs"; import os from "os"; import path from "path"; import { ConfigurationError } from "../../server/errors.js"; import { createAuthMiddleware } from "../../server/middleware/auth.js"; // ============================================ // Listen Address & Access Control // ============================================ export const DEFAULT_SERVER_PORT = 3000; /** Comma-separated API keys; when set, every route except health needs one. */ export const SERVER_API_KEY_ENV = "NEUROLINK_SERVER_API_KEY"; /** * Resolve the listen port: --port, then the config file, then the PORT * environment variable (what container platforms set), then 3000. */ export function resolveServerPort(cliPort, filePort) { const envPort = process.env.PORT?.trim(); const port = cliPort ?? filePort ?? (envPort ? Number(envPort) : DEFAULT_SERVER_PORT); if (!Number.isInteger(port) || port < 0 || port > 65535) { throw new ConfigurationError(`Invalid port "${cliPort ?? filePort ?? envPort}": expected an integer between 0 and 65535`); } return port; } export function readServerApiKeys() { return (process.env[SERVER_API_KEY_ENV] ?? "") .split(",") .map((key) => key.trim()) .filter((key) => key.length > 0); } /** * Constant-time for equal lengths; a length mismatch returns early, which * reveals only the key's length, never its content. */ const matchesKey = (key, token) => key.length === token.length && timingSafeEqual(key, token); /** * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`. * Health probes stay open so orchestrators can reach them, and the dev * playground header cannot bypass a key the operator configured. */ export function createServerApiKeyMiddleware(apiKeys, basePath) { const keys = apiKeys.map((key) => Buffer.from(key, "utf8")); return createAuthMiddleware({ type: "custom", extractToken: (ctx) => { const bearer = /^Bearer\s+(.+)$/i.exec(ctx.headers["authorization"] ?? ""); return bearer?.[1] ?? ctx.headers["x-api-key"] ?? null; }, validate: async (token) => { const candidate = Buffer.from(token, "utf8"); return keys.some((key) => matchesKey(key, candidate)) ? { id: "api-key" } : null; }, skipPaths: [`${basePath.replace(/\/+$/, "")}/health`], skipDevPlayground: false, }); } // ============================================ // State Directory Management // ============================================ /** * Get the base directory for NeuroLink state files * @returns Path to ~/.neurolink directory */ export function getNeuroLinkDir() { return path.join(os.homedir(), ".neurolink"); } /** * Ensure the NeuroLink state directory exists * Creates ~/.neurolink if it doesn't exist */ export function ensureStateDir() { const dir = getNeuroLinkDir(); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // ============================================ // Process Management // ============================================ /** * Check if a process with the given PID is currently running * * Uses `process.kill(pid, 0)` which tests if a process exists without sending a signal. * * **Platform Behavior:** * - **Unix/Linux/macOS**: Returns `true` if process exists, `false` if not. * If the process exists but belongs to another user, returns `true` (via EPERM check). * - **Windows**: Behavior differs - `process.kill(pid, 0)` may throw even for existing * processes if they are system processes or have restricted access. This function * handles EPERM by returning `true`, but other Windows-specific errors may occur. * For more reliable Windows process detection, consider using `tasklist` command. * * @param pid - Process ID to check * @returns true if the process is running, false otherwise */ export function isProcessRunning(pid) { try { // Sending signal 0 tests if process exists without actually sending a signal process.kill(pid, 0); return true; } catch (err) { const code = err.code; // EPERM means process exists but we lack permission to send signals to it return code === "EPERM"; } } /** * Whether a saved server state belongs to a different, still-running process. * A state naming our own PID is stale: a restarted container is PID 1 again * and would otherwise refuse to start, believing it is already running. */ export function isOtherServerRunning(pid) { return pid !== process.pid && isProcessRunning(pid); } /** * Remove this process's state file however it exits. A graceful stop can end * the process before its signal handler gets to clean up (once the listener * closes, the event loop may drain), so this runs from the "exit" event. */ export function clearStateOnExit(clearState) { process.once("exit", clearState); } // ============================================ // Time Formatting // ============================================ /** * Format a duration in milliseconds to a human-readable uptime string * @param ms - Duration in milliseconds * @returns Formatted string like "2d 5h 30m" or "45m 30s" */ export function formatUptime(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h ${minutes % 60}m`; } if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } return `${seconds}s`; } // ============================================ // Generic State File Management // ============================================ /** * Generic state file manager for server state persistence * @template T - Type of the state object */ export class StateFileManager { filePath; /** * Create a new state file manager * @param filename - Name of the state file (e.g., "serve-state.json") * @param baseDir - Optional base directory (defaults to ~/.neurolink) */ constructor(filename, baseDir) { this.filePath = path.join(baseDir ?? getNeuroLinkDir(), filename); } /** * Get the full path to the state file */ getFilePath() { return this.filePath; } /** * Save state to the state file * @param state - State object to save */ save(state) { const dir = path.dirname(this.filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 0o600, }); fs.chmodSync(this.filePath, 0o600); } /** * Load state from the state file * @returns The state object, or null if the file doesn't exist or is invalid */ load() { try { if (fs.existsSync(this.filePath)) { const content = fs.readFileSync(this.filePath, "utf8"); return JSON.parse(content); } } catch { // Ignore errors - return null for missing or invalid files } return null; } /** * Clear (delete) the state file */ clear() { try { if (fs.existsSync(this.filePath)) { fs.unlinkSync(this.filePath); } } catch { // Ignore errors when clearing } } /** * Check if state file exists */ exists() { return fs.existsSync(this.filePath); } } //# sourceMappingURL=serverUtils.js.map