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 108                                                      34x 34x 34x 34x 34x               34x             10x 10x   10x                 10x 10x 10x 5x 5x 5x 4x       10x             10x           10x   10x 2x               8x   8x                 8x        
import { DEFAULT_BASE_URL, USER_AGENT } from './consts';
import { STACKONE_HEADER_KEYS } from './headers';
import {
	type RpcActionRequest,
	type RpcActionResponse,
	type RpcClientConfig,
	rpcActionRequestSchema,
	rpcActionResponseSchema,
	rpcClientConfigSchema,
} from './schema';
import { StackOneAPIError } from './utils/error-stackone-api';
 
// 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 || DEFAULT_BASE_URL;
		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': USER_AGENT,
				...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;
		},
	};
}