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

292 lines (291 loc) • 8.95 kB
/** * Request Validation Utilities * Provides Zod schemas and validation helpers for server routes */ import { z } from "zod"; // ============================================ // Validation Schemas // ============================================ /** * Agent execute request schema */ export const AgentExecuteRequestSchema = z.object({ input: z.union([ z.string(), z.object({ text: z.string(), images: z.array(z.string()).optional(), files: z.array(z.string()).optional(), }), ]), provider: z.string().optional(), model: z.string().optional(), systemPrompt: z.string().optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().positive().optional(), tools: z.array(z.string()).optional(), stream: z.boolean().optional(), sessionId: z.string().optional(), userId: z.string().optional(), }); /** * Options a WebSocket `generate`/`stream` message may pass through to * `neurolink.generate()`/`.stream()`. Deliberately an allowlist, not the full * `GenerateOptions`/`StreamOptions` shape: the raw client JSON is untrusted, * and `GenerateOptions.credentials` in particular carries a per-provider * `baseURL` (see `NeurolinkCredentials`) that would otherwise let any caller * redirect the server's own outbound request to an arbitrary host (SSRF) or * swap in their own API key. `z.object` strips unknown keys by default, so * `credentials` and anything else outside this list never reaches the SDK * call — the same posture `AgentExecuteRequestSchema` already takes for the * equivalent HTTP route. */ export const WebSocketAgentOptionsSchema = z.object({ provider: z.string().optional(), model: z.string().optional(), systemPrompt: z.string().optional(), temperature: z.number().min(0).max(2).optional(), maxTokens: z.number().positive().optional(), maxSteps: z.number().positive().optional(), }); /** * WebSocket `generate`/`stream` message payload schema. */ export const WebSocketAgentRequestSchema = z.object({ prompt: z.string().min(1, "Prompt is required"), options: WebSocketAgentOptionsSchema.optional(), }); /** * Tool execute request schema */ export const ToolExecuteRequestSchema = z.object({ name: z.string().min(1, "Tool name is required"), arguments: z.record(z.string(), z.unknown()).default({}), sessionId: z.string().optional(), userId: z.string().optional(), }); /** * Tool arguments schema (for direct tool execution) */ export const ToolArgumentsSchema = z.record(z.string(), z.unknown()); /** * Memory session ID parameter schema */ export const SessionIdParamSchema = z.object({ sessionId: z.string().min(1, "Session ID is required"), }); /** * MCP server name parameter schema */ export const ServerNameParamSchema = z.object({ name: z.string().min(1, "Server name is required"), }); /** * Tool name parameter schema */ export const ToolNameParamSchema = z.object({ name: z.string().min(1, "Tool name is required"), }); /** * Tool search query schema */ export const ToolSearchQuerySchema = z.object({ q: z.string().optional(), source: z.string().optional(), limit: z .string() .transform((val) => parseInt(val, 10)) .pipe(z.number().positive().max(100)) .optional(), }); /** * Generic ID parameter schema (for session endpoints using :id) */ export const IdParamSchema = z.object({ id: z.string().min(1, "Session ID is required"), }); /** * Sessions list query schema (with optional pagination and filtering) */ export const SessionsListQuerySchema = z.object({ userId: z.string().optional(), limit: z .string() .transform((val) => parseInt(val, 10)) .pipe(z.number().positive().max(100)) .optional(), offset: z .string() .transform((val) => parseInt(val, 10)) .pipe(z.number().nonnegative()) .optional(), }); /** * Session messages query schema (for pagination) */ export const SessionMessagesQuerySchema = z.object({ limit: z .string() .transform((val) => parseInt(val, 10)) .pipe(z.number().positive().max(100)) .optional(), offset: z .string() .transform((val) => parseInt(val, 10)) .pipe(z.number().nonnegative()) .optional(), }); /** * Embed request schema (single text) */ export const EmbedRequestSchema = z.object({ text: z.string().min(1, "Text is required"), provider: z.string().optional(), model: z.string().optional(), }); /** * Embed many request schema (batch texts) */ export const EmbedManyRequestSchema = z.object({ texts: z .array(z.string().min(1)) .min(1, "At least one text is required") .max(2048, "Maximum 2048 texts per batch"), provider: z.string().optional(), model: z.string().optional(), }); /** * Skill create request schema */ export const SkillCreateRequestSchema = z.object({ name: z.string().min(1, "Name is required"), displayName: z.string().optional(), description: z.string().min(1, "Description is required"), instructions: z.string().min(1, "Instructions are required"), tags: z.array(z.string()).optional(), scope: z.enum(["global", "scoped"]).optional(), scopeIds: z.array(z.string()).optional(), requestedBy: z.string().optional(), }); /** * Skill update request schema — all fields optional (patch semantics) */ export const SkillUpdateRequestSchema = SkillCreateRequestSchema.partial(); // ============================================ // Error Response Type Guards / Helpers // ============================================ /** * Type guard to check if a value is an ErrorResponse */ export function isErrorResponse(value) { return (typeof value === "object" && value !== null && "error" in value && typeof value.error === "object" && value.error !== null && "code" in value.error && "message" in value.error); } /** * Get default HTTP status code based on error code */ function getDefaultHttpStatus(code) { const statusMap = { VALIDATION_ERROR: 400, SCHEMA_ERROR: 400, TOOL_NOT_FOUND: 404, SERVER_NOT_FOUND: 404, SESSION_NOT_FOUND: 404, NOT_FOUND: 404, AUTH_REQUIRED: 401, AUTH_INVALID: 401, FORBIDDEN: 403, RATE_LIMIT_EXCEEDED: 429, MCP_UNAVAILABLE: 503, MEMORY_UNAVAILABLE: 503, SKILLS_UNAVAILABLE: 503, EXECUTION_FAILED: 500, INTERNAL_ERROR: 500, }; return statusMap[code] ?? 500; } // ============================================ // Validation Helpers // ============================================ /** * Create a standardized error response */ export function createErrorResponse(code, message, details, requestId, httpStatus) { return { error: { code, message, details, }, metadata: { timestamp: new Date().toISOString(), requestId, }, httpStatus: httpStatus ?? getDefaultHttpStatus(code), }; } /** * Validate request body against a Zod schema */ export function validateRequest(schema, data, requestId) { const result = schema.safeParse(data); if (!result.success) { return { success: false, error: createErrorResponse("VALIDATION_ERROR", "Invalid request body", result.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code, })), requestId), }; } return { success: true, data: result.data, }; } /** * Validate query parameters against a Zod schema */ export function validateQuery(schema, query, requestId) { const result = schema.safeParse(query); if (!result.success) { return { success: false, error: createErrorResponse("VALIDATION_ERROR", "Invalid query parameters", result.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code, })), requestId), }; } return { success: true, data: result.data, }; } /** * Validate path parameters against a Zod schema */ export function validateParams(schema, params, requestId) { const result = schema.safeParse(params); if (!result.success) { return { success: false, error: createErrorResponse("VALIDATION_ERROR", "Invalid path parameters", result.error.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message, code: issue.code, })), requestId), }; } return { success: true, data: result.data, }; }