/** * JSON-RPC 2.0 message types and protocol helpers for the host ↔ worker IPC * channel. * * The Paperclip plugin runtime uses JSON-RPC 2.0 over stdio to communicate * between the host process and each plugin worker process. This module defines: * * - Core JSON-RPC 2.0 envelope types (request, response, notification, error) * - Standard and plugin-specific error codes * - Typed method maps for host→worker and worker→host calls * - Helper functions for creating well-formed messages * * @see PLUGIN_SPEC.md §12.1 — Process Model * @see PLUGIN_SPEC.md §13 — Host-Worker Protocol * @see https://www.jsonrpc.org/specification */ import type { PaperclipPluginManifestV1, PluginLauncherBounds, PluginLauncherRenderContextSnapshot, PluginLauncherRenderEnvironment, PluginStateScopeKind, Company, Project, Issue, IssueComment, IssueDocument, IssueDocumentSummary, Agent, Goal, } from "@paperclipai/shared"; export type { PluginLauncherRenderContextSnapshot } from "@paperclipai/shared"; import type { PluginEvent, PluginJobContext, PluginWorkspace, ToolRunContext, ToolResult, } from "./types.js"; import type { PluginHealthDiagnostics, PluginConfigValidationResult, PluginWebhookInput, } from "./define-plugin.js"; // --------------------------------------------------------------------------- // JSON-RPC 2.0 — Core Protocol Types // --------------------------------------------------------------------------- /** The JSON-RPC protocol version. Always `"2.0"`. */ export const JSONRPC_VERSION = "2.0" as const; /** * A unique request identifier. JSON-RPC 2.0 allows strings or numbers; * we use strings (UUIDs or monotonic counters) for all Paperclip messages. */ export type JsonRpcId = string | number; /** * A JSON-RPC 2.0 request message. * * The host sends requests to the worker (or vice versa) and expects a * matching response with the same `id`. */ export interface JsonRpcRequest< TMethod extends string = string, TParams = unknown, > { readonly jsonrpc: typeof JSONRPC_VERSION; /** Unique request identifier. Must be echoed in the response. */ readonly id: JsonRpcId; /** The RPC method name to invoke. */ readonly method: TMethod; /** Structured parameters for the method call. */ readonly params: TParams; } /** * A JSON-RPC 2.0 success response. */ export interface JsonRpcSuccessResponse { readonly jsonrpc: typeof JSONRPC_VERSION; /** Echoed request identifier. */ readonly id: JsonRpcId; /** The method return value. */ readonly result: TResult; readonly error?: never; } /** * A JSON-RPC 2.0 error object embedded in an error response. */ export interface JsonRpcError { /** Machine-readable error code. */ readonly code: number; /** Human-readable error message. */ readonly message: string; /** Optional structured error data. */ readonly data?: TData; } /** * A JSON-RPC 2.0 error response. */ export interface JsonRpcErrorResponse { readonly jsonrpc: typeof JSONRPC_VERSION; /** Echoed request identifier. */ readonly id: JsonRpcId | null; readonly result?: never; /** The error object. */ readonly error: JsonRpcError; } /** * A JSON-RPC 2.0 response — either success or error. */ export type JsonRpcResponse = | JsonRpcSuccessResponse | JsonRpcErrorResponse; /** * A JSON-RPC 2.0 notification (a request with no `id`). * * Notifications are fire-and-forget — no response is expected. */ export interface JsonRpcNotification< TMethod extends string = string, TParams = unknown, > { readonly jsonrpc: typeof JSONRPC_VERSION; readonly id?: never; /** The notification method name. */ readonly method: TMethod; /** Structured parameters for the notification. */ readonly params: TParams; } /** * Any well-formed JSON-RPC 2.0 message (request, response, or notification). */ export type JsonRpcMessage = | JsonRpcRequest | JsonRpcResponse | JsonRpcNotification; // --------------------------------------------------------------------------- // Error Codes // --------------------------------------------------------------------------- /** * Standard JSON-RPC 2.0 error codes. * * @see https://www.jsonrpc.org/specification#error_object */ export const JSONRPC_ERROR_CODES = { /** Invalid JSON was received by the server. */ PARSE_ERROR: -32700, /** The JSON sent is not a valid Request object. */ INVALID_REQUEST: -32600, /** The method does not exist or is not available. */ METHOD_NOT_FOUND: -32601, /** Invalid method parameter(s). */ INVALID_PARAMS: -32602, /** Internal JSON-RPC error. */ INTERNAL_ERROR: -32603, } as const; export type JsonRpcErrorCode = (typeof JSONRPC_ERROR_CODES)[keyof typeof JSONRPC_ERROR_CODES]; /** * Paperclip plugin-specific error codes. * * These live in the JSON-RPC "server error" reserved range (-32000 to -32099) * as specified by JSON-RPC 2.0 for implementation-defined server errors. * * @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge */ export const PLUGIN_RPC_ERROR_CODES = { /** The worker process is not running or not reachable. */ WORKER_UNAVAILABLE: -32000, /** The plugin does not have the required capability for this operation. */ CAPABILITY_DENIED: -32001, /** The worker reported an unhandled error during method execution. */ WORKER_ERROR: -32002, /** The method call timed out waiting for the worker response. */ TIMEOUT: -32003, /** The worker does not implement the requested optional method. */ METHOD_NOT_IMPLEMENTED: -32004, /** A catch-all for errors that do not fit other categories. */ UNKNOWN: -32099, } as const; export type PluginRpcErrorCode = (typeof PLUGIN_RPC_ERROR_CODES)[keyof typeof PLUGIN_RPC_ERROR_CODES]; // --------------------------------------------------------------------------- // Host → Worker Method Signatures (§13 Host-Worker Protocol) // --------------------------------------------------------------------------- /** * Input for the `initialize` RPC method. * * @see PLUGIN_SPEC.md §13.1 — `initialize` */ export interface InitializeParams { /** Full plugin manifest snapshot. */ manifest: PaperclipPluginManifestV1; /** Resolved operator configuration (validated against `instanceConfigSchema`). */ config: Record; /** Instance-level metadata. */ instanceInfo: { /** UUID of this Paperclip instance. */ instanceId: string; /** Semver version of the running Paperclip host. */ hostVersion: string; }; /** Host API version. */ apiVersion: number; } /** * Result returned by the `initialize` RPC method. */ export interface InitializeResult { /** Whether initialization succeeded. */ ok: boolean; /** Optional methods the worker has implemented (e.g. "validateConfig", "onEvent"). */ supportedMethods?: string[]; } /** * Input for the `configChanged` RPC method. * * @see PLUGIN_SPEC.md §13.4 — `configChanged` */ export interface ConfigChangedParams { /** The newly resolved configuration. */ config: Record; } /** * Input for the `validateConfig` RPC method. * * @see PLUGIN_SPEC.md §13.3 — `validateConfig` */ export interface ValidateConfigParams { /** The configuration to validate. */ config: Record; } /** * Input for the `onEvent` RPC method. * * @see PLUGIN_SPEC.md §13.5 — `onEvent` */ export interface OnEventParams { /** The domain event to deliver. */ event: PluginEvent; } /** * Input for the `runJob` RPC method. * * @see PLUGIN_SPEC.md §13.6 — `runJob` */ export interface RunJobParams { /** Job execution context. */ job: PluginJobContext; } /** * Input for the `getData` RPC method. * * @see PLUGIN_SPEC.md §13.8 — `getData` */ export interface GetDataParams { /** Plugin-defined data key (e.g. `"sync-health"`). */ key: string; /** Context and query parameters from the UI. */ params: Record; /** Optional launcher/container metadata from the host render environment. */ renderEnvironment?: PluginLauncherRenderContextSnapshot | null; } /** * Input for the `performAction` RPC method. * * @see PLUGIN_SPEC.md §13.9 — `performAction` */ export interface PerformActionParams { /** Plugin-defined action key (e.g. `"resync"`). */ key: string; /** Action parameters from the UI. */ params: Record; /** Optional launcher/container metadata from the host render environment. */ renderEnvironment?: PluginLauncherRenderContextSnapshot | null; } /** * Input for the `executeTool` RPC method. * * @see PLUGIN_SPEC.md §13.10 — `executeTool` */ export interface ExecuteToolParams { /** Tool name (without plugin namespace prefix). */ toolName: string; /** Parsed parameters matching the tool's declared schema. */ parameters: unknown; /** Agent run context. */ runContext: ToolRunContext; } // --------------------------------------------------------------------------- // UI launcher / modal host interaction payloads // --------------------------------------------------------------------------- /** * Bounds request issued by a plugin UI running inside a host-managed launcher * container such as a modal, drawer, or popover. */ export interface PluginModalBoundsRequest { /** High-level size preset requested from the host. */ bounds: PluginLauncherBounds; /** Optional explicit width override in CSS pixels. */ width?: number; /** Optional explicit height override in CSS pixels. */ height?: number; /** Optional lower bounds for host resizing decisions. */ minWidth?: number; minHeight?: number; /** Optional upper bounds for host resizing decisions. */ maxWidth?: number; maxHeight?: number; } /** * Reason metadata supplied by host-managed close lifecycle callbacks. */ export interface PluginRenderCloseEvent { reason: | "escapeKey" | "backdrop" | "hostNavigation" | "programmatic" | "submit" | "unknown"; nativeEvent?: unknown; } /** * Map of host→worker RPC method names to their `[params, result]` types. * * This type is the single source of truth for all methods the host can call * on a worker. Used by both the host dispatcher and the worker handler to * ensure type safety across the IPC boundary. */ export interface HostToWorkerMethods { /** @see PLUGIN_SPEC.md §13.1 */ initialize: [params: InitializeParams, result: InitializeResult]; /** @see PLUGIN_SPEC.md §13.2 */ health: [params: Record, result: PluginHealthDiagnostics]; /** @see PLUGIN_SPEC.md §12.5 */ shutdown: [params: Record, result: void]; /** @see PLUGIN_SPEC.md §13.3 */ validateConfig: [params: ValidateConfigParams, result: PluginConfigValidationResult]; /** @see PLUGIN_SPEC.md §13.4 */ configChanged: [params: ConfigChangedParams, result: void]; /** @see PLUGIN_SPEC.md §13.5 */ onEvent: [params: OnEventParams, result: void]; /** @see PLUGIN_SPEC.md §13.6 */ runJob: [params: RunJobParams, result: void]; /** @see PLUGIN_SPEC.md §13.7 */ handleWebhook: [params: PluginWebhookInput, result: void]; /** @see PLUGIN_SPEC.md §13.8 */ getData: [params: GetDataParams, result: unknown]; /** @see PLUGIN_SPEC.md §13.9 */ performAction: [params: PerformActionParams, result: unknown]; /** @see PLUGIN_SPEC.md §13.10 */ executeTool: [params: ExecuteToolParams, result: ToolResult]; } /** Union of all host→worker method names. */ export type HostToWorkerMethodName = keyof HostToWorkerMethods; /** Required methods the worker MUST implement. */ export const HOST_TO_WORKER_REQUIRED_METHODS: readonly HostToWorkerMethodName[] = [ "initialize", "health", "shutdown", ] as const; /** Optional methods the worker MAY implement. */ export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[] = [ "validateConfig", "configChanged", "onEvent", "runJob", "handleWebhook", "getData", "performAction", "executeTool", ] as const; // --------------------------------------------------------------------------- // Worker → Host Method Signatures (SDK client calls) // --------------------------------------------------------------------------- /** * Map of worker→host RPC method names to their `[params, result]` types. * * These represent the SDK client calls that the worker makes back to the * host to access platform services (state, entities, config, etc.). */ export interface WorkerToHostMethods { // Config "config.get": [params: Record, result: Record]; // State "state.get": [ params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string }, result: unknown, ]; "state.set": [ params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string; value: unknown }, result: void, ]; "state.delete": [ params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string }, result: void, ]; // Entities "entities.upsert": [ params: { entityType: string; scopeKind: PluginStateScopeKind; scopeId?: string; externalId?: string; title?: string; status?: string; data: Record; }, result: { id: string; entityType: string; scopeKind: PluginStateScopeKind; scopeId: string | null; externalId: string | null; title: string | null; status: string | null; data: Record; createdAt: string; updatedAt: string; }, ]; "entities.list": [ params: { entityType?: string; scopeKind?: PluginStateScopeKind; scopeId?: string; externalId?: string; limit?: number; offset?: number; }, result: Array<{ id: string; entityType: string; scopeKind: PluginStateScopeKind; scopeId: string | null; externalId: string | null; title: string | null; status: string | null; data: Record; createdAt: string; updatedAt: string; }>, ]; // Events "events.emit": [ params: { name: string; companyId: string; payload: unknown }, result: void, ]; "events.subscribe": [ params: { eventPattern: string; filter?: Record | null }, result: void, ]; // HTTP "http.fetch": [ params: { url: string; init?: Record }, result: { status: number; statusText: string; headers: Record; body: string }, ]; // Secrets "secrets.resolve": [ params: { secretRef: string }, result: string, ]; // Activity "activity.log": [ params: { companyId: string; message: string; entityType?: string; entityId?: string; metadata?: Record; }, result: void, ]; // Metrics "metrics.write": [ params: { name: string; value: number; tags?: Record }, result: void, ]; // Logger "log": [ params: { level: "info" | "warn" | "error" | "debug"; message: string; meta?: Record }, result: void, ]; // Companies (read) "companies.list": [ params: { limit?: number; offset?: number }, result: Company[], ]; "companies.get": [ params: { companyId: string }, result: Company | null, ]; // Projects (read) "projects.list": [ params: { companyId: string; limit?: number; offset?: number }, result: Project[], ]; "projects.get": [ params: { projectId: string; companyId: string }, result: Project | null, ]; "projects.listWorkspaces": [ params: { projectId: string; companyId: string }, result: PluginWorkspace[], ]; "projects.getPrimaryWorkspace": [ params: { projectId: string; companyId: string }, result: PluginWorkspace | null, ]; "projects.getWorkspaceForIssue": [ params: { issueId: string; companyId: string }, result: PluginWorkspace | null, ]; // Issues "issues.list": [ params: { companyId: string; projectId?: string; assigneeAgentId?: string; status?: string; limit?: number; offset?: number; }, result: Issue[], ]; "issues.get": [ params: { issueId: string; companyId: string }, result: Issue | null, ]; "issues.create": [ params: { companyId: string; projectId?: string; goalId?: string; parentId?: string; title: string; description?: string; priority?: string; assigneeAgentId?: string; }, result: Issue, ]; "issues.update": [ params: { issueId: string; patch: Record; companyId: string; }, result: Issue, ]; "issues.listComments": [ params: { issueId: string; companyId: string }, result: IssueComment[], ]; "issues.createComment": [ params: { issueId: string; body: string; companyId: string }, result: IssueComment, ]; // Issue Documents "issues.documents.list": [ params: { issueId: string; companyId: string }, result: IssueDocumentSummary[], ]; "issues.documents.get": [ params: { issueId: string; key: string; companyId: string }, result: IssueDocument | null, ]; "issues.documents.upsert": [ params: { issueId: string; key: string; body: string; companyId: string; title?: string; format?: string; changeSummary?: string; }, result: IssueDocument, ]; "issues.documents.delete": [ params: { issueId: string; key: string; companyId: string }, result: void, ]; // Agents (read) "agents.list": [ params: { companyId: string; status?: string; limit?: number; offset?: number }, result: Agent[], ]; "agents.get": [ params: { agentId: string; companyId: string }, result: Agent | null, ]; // Agents (write) "agents.pause": [ params: { agentId: string; companyId: string }, result: Agent, ]; "agents.resume": [ params: { agentId: string; companyId: string }, result: Agent, ]; "agents.invoke": [ params: { agentId: string; companyId: string; prompt: string; reason?: string }, result: { runId: string }, ]; // Agent Sessions "agents.sessions.create": [ params: { agentId: string; companyId: string; taskKey?: string; reason?: string }, result: { sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string }, ]; "agents.sessions.list": [ params: { agentId: string; companyId: string }, result: Array<{ sessionId: string; agentId: string; companyId: string; status: "active" | "closed"; createdAt: string }>, ]; "agents.sessions.sendMessage": [ params: { sessionId: string; companyId: string; prompt: string; reason?: string }, result: { runId: string }, ]; "agents.sessions.close": [ params: { sessionId: string; companyId: string }, result: void, ]; // Goals "goals.list": [ params: { companyId: string; level?: string; status?: string; limit?: number; offset?: number }, result: Goal[], ]; "goals.get": [ params: { goalId: string; companyId: string }, result: Goal | null, ]; "goals.create": [ params: { companyId: string; title: string; description?: string; level?: string; status?: string; parentId?: string; ownerAgentId?: string; }, result: Goal, ]; "goals.update": [ params: { goalId: string; patch: Record; companyId: string; }, result: Goal, ]; } /** Union of all worker→host method names. */ export type WorkerToHostMethodName = keyof WorkerToHostMethods; // --------------------------------------------------------------------------- // Worker→Host Notification Types (fire-and-forget, no response) // --------------------------------------------------------------------------- /** * Typed parameter shapes for worker→host JSON-RPC notifications. * * Notifications are fire-and-forget — the worker does not wait for a response. * These are used for streaming events and logging, not for request-response RPCs. */ export interface WorkerToHostNotifications { /** * Forward a stream event to connected SSE clients. * * Emitted by the worker for each event on a stream channel. The host * publishes to the PluginStreamBus, which fans out to all SSE clients * subscribed to the (pluginId, channel, companyId) tuple. * * The `event` payload is JSON-serializable and sent as SSE `data:`. * The default SSE event type is `"message"`. */ "streams.emit": { channel: string; companyId: string; event: unknown; }; /** * Signal that a stream channel has been opened. * * Emitted when the worker calls `ctx.streams.open(channel, companyId)`. * UI clients may use this to display a "connected" indicator or begin * buffering input. The host tracks open channels so it can emit synthetic * close events if the worker crashes. */ "streams.open": { channel: string; companyId: string; }; /** * Signal that a stream channel has been closed. * * Emitted when the worker calls `ctx.streams.close(channel)`, or * synthetically by the host when a worker process exits with channels * still open. UI clients should treat this as terminal and disconnect * the SSE connection. */ "streams.close": { channel: string; companyId: string; }; } /** Union of all worker→host notification method names. */ export type WorkerToHostNotificationName = keyof WorkerToHostNotifications; // --------------------------------------------------------------------------- // Typed Request / Response Helpers // --------------------------------------------------------------------------- /** * A typed JSON-RPC request for a specific host→worker method. */ export type HostToWorkerRequest = JsonRpcRequest; /** * A typed JSON-RPC success response for a specific host→worker method. */ export type HostToWorkerResponse = JsonRpcSuccessResponse; /** * A typed JSON-RPC request for a specific worker→host method. */ export type WorkerToHostRequest = JsonRpcRequest; /** * A typed JSON-RPC success response for a specific worker→host method. */ export type WorkerToHostResponse = JsonRpcSuccessResponse; // --------------------------------------------------------------------------- // Message Factory Functions // --------------------------------------------------------------------------- /** Counter for generating unique request IDs when no explicit ID is provided. */ let _nextId = 1; /** Wrap around before reaching Number.MAX_SAFE_INTEGER to prevent precision loss. */ const MAX_SAFE_RPC_ID = Number.MAX_SAFE_INTEGER - 1; /** * Create a JSON-RPC 2.0 request message. * * @param method - The RPC method name * @param params - Structured parameters * @param id - Optional explicit request ID (auto-generated if omitted) */ export function createRequest( method: TMethod, params: unknown, id?: JsonRpcId, ): JsonRpcRequest { if (_nextId >= MAX_SAFE_RPC_ID) { _nextId = 1; } return { jsonrpc: JSONRPC_VERSION, id: id ?? _nextId++, method, params, }; } /** * Create a JSON-RPC 2.0 success response. * * @param id - The request ID being responded to * @param result - The result value */ export function createSuccessResponse( id: JsonRpcId, result: TResult, ): JsonRpcSuccessResponse { return { jsonrpc: JSONRPC_VERSION, id, result, }; } /** * Create a JSON-RPC 2.0 error response. * * @param id - The request ID being responded to (null if the request ID could not be determined) * @param code - Machine-readable error code * @param message - Human-readable error message * @param data - Optional structured error data */ export function createErrorResponse( id: JsonRpcId | null, code: number, message: string, data?: TData, ): JsonRpcErrorResponse { const response: JsonRpcErrorResponse = { jsonrpc: JSONRPC_VERSION, id, error: data !== undefined ? { code, message, data } : { code, message } as JsonRpcError, }; return response; } /** * Create a JSON-RPC 2.0 notification (fire-and-forget, no response expected). * * @param method - The notification method name * @param params - Structured parameters */ export function createNotification( method: TMethod, params: unknown, ): JsonRpcNotification { return { jsonrpc: JSONRPC_VERSION, method, params, }; } // --------------------------------------------------------------------------- // Type Guards // --------------------------------------------------------------------------- /** * Check whether a value is a well-formed JSON-RPC 2.0 request. * * A request has `jsonrpc: "2.0"`, a string `method`, and an `id`. */ export function isJsonRpcRequest(value: unknown): value is JsonRpcRequest { if (typeof value !== "object" || value === null) return false; const obj = value as Record; return ( obj.jsonrpc === JSONRPC_VERSION && typeof obj.method === "string" && "id" in obj && obj.id !== undefined && obj.id !== null ); } /** * Check whether a value is a well-formed JSON-RPC 2.0 notification. * * A notification has `jsonrpc: "2.0"`, a string `method`, but no `id`. */ export function isJsonRpcNotification( value: unknown, ): value is JsonRpcNotification { if (typeof value !== "object" || value === null) return false; const obj = value as Record; return ( obj.jsonrpc === JSONRPC_VERSION && typeof obj.method === "string" && !("id" in obj) ); } /** * Check whether a value is a well-formed JSON-RPC 2.0 response (success or error). */ export function isJsonRpcResponse(value: unknown): value is JsonRpcResponse { if (typeof value !== "object" || value === null) return false; const obj = value as Record; return ( obj.jsonrpc === JSONRPC_VERSION && "id" in obj && ("result" in obj || "error" in obj) ); } /** * Check whether a JSON-RPC response is a success response. */ export function isJsonRpcSuccessResponse( response: JsonRpcResponse, ): response is JsonRpcSuccessResponse { return "result" in response && !("error" in response && response.error !== undefined); } /** * Check whether a JSON-RPC response is an error response. */ export function isJsonRpcErrorResponse( response: JsonRpcResponse, ): response is JsonRpcErrorResponse { return "error" in response && response.error !== undefined; } // --------------------------------------------------------------------------- // Serialization Helpers // --------------------------------------------------------------------------- /** * Line delimiter for JSON-RPC messages over stdio. * * Each message is a single line of JSON terminated by a newline character. * This follows the newline-delimited JSON (NDJSON) convention. */ export const MESSAGE_DELIMITER = "\n" as const; /** * Serialize a JSON-RPC message to a newline-delimited string for transmission * over stdio. * * @param message - Any JSON-RPC message (request, response, or notification) * @returns The JSON string terminated with a newline */ export function serializeMessage(message: JsonRpcMessage): string { return JSON.stringify(message) + MESSAGE_DELIMITER; } /** * Parse a JSON string into a JSON-RPC message. * * Returns the parsed message or throws a `JsonRpcParseError` if the input * is not valid JSON or does not conform to the JSON-RPC 2.0 structure. * * @param line - A single line of JSON text (with or without trailing newline) * @returns The parsed JSON-RPC message * @throws {JsonRpcParseError} If parsing fails */ export function parseMessage(line: string): JsonRpcMessage { const trimmed = line.trim(); if (trimmed.length === 0) { throw new JsonRpcParseError("Empty message"); } let parsed: unknown; try { parsed = JSON.parse(trimmed); } catch { throw new JsonRpcParseError(`Invalid JSON: ${trimmed.slice(0, 200)}`); } if (typeof parsed !== "object" || parsed === null) { throw new JsonRpcParseError("Message must be a JSON object"); } const obj = parsed as Record; if (obj.jsonrpc !== JSONRPC_VERSION) { throw new JsonRpcParseError( `Invalid or missing jsonrpc version (expected "${JSONRPC_VERSION}", got ${JSON.stringify(obj.jsonrpc)})`, ); } // It's a valid JSON-RPC 2.0 envelope — return as-is and let the caller // use the type guards for more specific classification. return parsed as JsonRpcMessage; } // --------------------------------------------------------------------------- // Error Classes // --------------------------------------------------------------------------- /** * Error thrown when a JSON-RPC message cannot be parsed. */ export class JsonRpcParseError extends Error { override readonly name = "JsonRpcParseError"; constructor(message: string) { super(message); } } /** * Error thrown when a JSON-RPC call fails with a structured error response. * * Captures the full `JsonRpcError` so callers can inspect the code and data. */ export class JsonRpcCallError extends Error { override readonly name = "JsonRpcCallError"; /** The JSON-RPC error code. */ readonly code: number; /** Optional structured error data from the response. */ readonly data: unknown; constructor(error: JsonRpcError) { super(error.message); this.code = error.code; this.data = error.data; } } // --------------------------------------------------------------------------- // Reset helper (testing only) // --------------------------------------------------------------------------- /** * Reset the internal request ID counter. **For testing only.** * * @internal */ export function _resetIdCounter(): void { _nextId = 1; }