@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
JavaScript
/**
* 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,
};
}