All files / src rpc-client.ts

96% Statements 24/25
91.66% Branches 11/12
100% Functions 2/2
96% Lines 24/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107                                                    17x 17x 17x 17x 17x               17x             7x 7x   7x                 7x 7x 7x 2x 2x 2x 1x       7x             7x           7x   7x 2x               5x   5x                 5x        
import { STACKONE_HEADER_KEYS } from './headers';
import {
	type RpcActionRequest,
	type RpcActionResponse,
	type RpcClientConfig,
	rpcActionRequestSchema,
	rpcActionResponseSchema,
	rpcClientConfigSchema,
} from './schema';
import { StackOneAPIError } from './utils/errors';
 
// Re-export types for consumers and to make types portable
export type { RpcActionResponse } from './schema';
 
/**
 * Custom RPC client for StackOne API.
 * Replaces the @stackone/stackone-client-ts dependency.
 *
 * @see https://docs.stackone.com/platform/api-reference/actions/list-all-actions-metadata
 * @see https://docs.stackone.com/platform/api-reference/actions/make-an-rpc-call-to-an-action
 */
export class RpcClient {
	private readonly baseUrl: string;
	private readonly authHeader: string;
 
	constructor(config: RpcClientConfig) {
		const validatedConfig = rpcClientConfigSchema.parse(config);
		this.baseUrl = validatedConfig.serverURL || 'https://api.stackone.com';
		const username = validatedConfig.security.username;
		const password = validatedConfig.security.password || '';
		this.authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
	}
 
	/**
	 * Actions namespace containing RPC methods
	 */
	readonly actions: {
		rpcAction: (request: RpcActionRequest) => Promise<RpcActionResponse>;
	} = {
		/**
		 * Execute an RPC action
		 * @param request The RPC action request
		 * @returns The RPC action response matching server's ActionsRpcResponseApiModel
		 */
		rpcAction: async (request: RpcActionRequest): Promise<RpcActionResponse> => {
			const validatedRequest = rpcActionRequestSchema.parse(request);
			const url = `${this.baseUrl}/actions/rpc`;
 
			const requestBody = {
				action: validatedRequest.action,
				body: validatedRequest.body,
				headers: validatedRequest.headers,
				path: validatedRequest.path,
				query: validatedRequest.query,
			} as const satisfies RpcActionRequest;
 
			// Forward StackOne-specific headers as HTTP headers
			const requestHeaders = validatedRequest.headers;
			const forwardedHeaders: Record<string, string> = {};
			if (requestHeaders) {
				for (const key of STACKONE_HEADER_KEYS) {
					const value = requestHeaders[key];
					if (value !== undefined) {
						forwardedHeaders[key] = value;
					}
				}
			}
			const httpHeaders = {
				'Content-Type': 'application/json',
				Authorization: this.authHeader,
				'User-Agent': 'stackone-ai-node',
				...forwardedHeaders,
			} satisfies Record<string, string>;
 
			const response = await fetch(url, {
				method: 'POST',
				headers: httpHeaders,
				body: JSON.stringify(requestBody),
			});
 
			const responseBody: unknown = await response.json();
 
			if (!response.ok) {
				throw new StackOneAPIError(
					`RPC action failed for ${url}`,
					response.status,
					responseBody,
					requestBody,
				);
			}
 
			const validation = rpcActionResponseSchema.safeParse(responseBody);
 
			Iif (!validation.success) {
				throw new StackOneAPIError(
					`Invalid RPC action response for ${url}`,
					response.status,
					responseBody,
					requestBody,
				);
			}
 
			return validation.data;
		},
	};
}