UNPKG

@stackone/ai

Version:

Tools for agents to perform actions on your SaaS

208 lines (206 loc) 6.65 kB
import { ParameterLocation } from "../types.js"; import { StackOneAPIError } from "../utils/errors.js"; //#region src/modules/requestBuilder.ts var ParameterSerializationError = class extends Error { constructor(message) { super(message); this.name = "ParameterSerializationError"; } }; /** * Builds and executes HTTP requests */ var RequestBuilder = class { method; url; bodyType; params; headers; constructor(config, headers = {}) { this.method = config.method; this.url = config.url; this.bodyType = config.bodyType; this.params = config.params; this.headers = { ...headers }; } /** * Set headers for the request */ setHeaders(headers) { this.headers = { ...this.headers, ...headers }; return this; } /** * Get the current headers */ getHeaders() { return { ...this.headers }; } /** * Prepare headers for the API request */ prepareHeaders() { return { "User-Agent": "stackone-ai-node", ...this.headers }; } /** * Prepare URL and parameters for the API request */ prepareRequestParams(params) { let url = this.url; const bodyParams = {}; const queryParams = {}; for (const [key, value] of Object.entries(params)) { const paramConfig = this.params.find((p) => p.name === key); const paramLocation = paramConfig?.location; switch (paramLocation) { case ParameterLocation.PATH: url = url.replace(`{${key}}`, encodeURIComponent(String(value))); break; case ParameterLocation.QUERY: queryParams[key] = value; break; case ParameterLocation.HEADER: this.headers[key] = String(value); break; case ParameterLocation.BODY: bodyParams[key] = value; break; default: bodyParams[key] = value; break; } } return [ url, bodyParams, queryParams ]; } /** * Build the fetch options for the request */ buildFetchOptions(bodyParams) { const headers = this.prepareHeaders(); const fetchOptions = { method: this.method, headers }; if (Object.keys(bodyParams).length > 0) switch (this.bodyType) { case "json": fetchOptions.headers = { ...fetchOptions.headers, "Content-Type": "application/json" }; fetchOptions.body = JSON.stringify(bodyParams); break; case "form": { fetchOptions.headers = { ...fetchOptions.headers, "Content-Type": "application/x-www-form-urlencoded" }; const formBody = new URLSearchParams(); for (const [key, value] of Object.entries(bodyParams)) formBody.append(key, String(value)); fetchOptions.body = formBody.toString(); break; } case "multipart-form": { const formData = new FormData(); for (const [key, value] of Object.entries(bodyParams)) formData.append(key, String(value)); fetchOptions.body = formData; break; } } return fetchOptions; } /** * Validates parameter keys to prevent injection attacks */ validateParameterKey(key) { if (!/^[a-zA-Z0-9_.-]+$/.test(key)) throw new ParameterSerializationError(`Invalid parameter key: ${key}`); } /** * Safely serializes values to strings with special type handling */ serializeValue(value) { if (value instanceof Date) return value.toISOString(); if (value instanceof RegExp) return value.toString(); if (typeof value === "function") throw new ParameterSerializationError("Functions cannot be serialized as parameters"); if (value === null || value === void 0) return ""; return String(value); } /** * Serialize an object into deep object query parameters with security protections * Converts {filter: {updated_after: "2020-01-01", job_id: "123"}} * to filter[updated_after]=2020-01-01&filter[job_id]=123 */ serializeDeepObject(obj, prefix, depth = 0, visited = /* @__PURE__ */ new WeakSet(), options = {}) { const maxDepth = options.maxDepth ?? 10; const strictValidation = options.strictValidation ?? true; const params = []; if (depth > maxDepth) throw new ParameterSerializationError(`Maximum nesting depth (${maxDepth}) exceeded for parameter serialization`); if (obj === null || obj === void 0) return params; if (typeof obj === "object" && !Array.isArray(obj)) { if (visited.has(obj)) throw new ParameterSerializationError("Circular reference detected in parameter object"); visited.add(obj); try { for (const [key, value] of Object.entries(obj)) { if (strictValidation) this.validateParameterKey(key); const nestedKey = `${prefix}[${key}]`; if (value !== null && value !== void 0) if (this.shouldUseDeepObjectSerialization(key, value)) params.push(...this.serializeDeepObject(value, nestedKey, depth + 1, visited, options)); else params.push([nestedKey, this.serializeValue(value)]); } } finally { visited.delete(obj); } } else params.push([prefix, this.serializeValue(obj)]); return params; } /** * Check if a parameter should use deep object serialization * Applies to all plain object parameters (excludes special types and arrays) */ shouldUseDeepObjectSerialization(_key, value) { return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && typeof value !== "function"; } /** * Builds all query parameters with optimized batching */ buildQueryParameters(queryParams) { const allParams = []; for (const [key, value] of Object.entries(queryParams)) if (this.shouldUseDeepObjectSerialization(key, value)) allParams.push(...this.serializeDeepObject(value, key)); else allParams.push([key, this.serializeValue(value)]); return allParams; } /** * Execute the request */ async execute(params, options) { const [url, bodyParams, queryParams] = this.prepareRequestParams(params); const urlWithQuery = new URL(url); const serializedParams = this.buildQueryParameters(queryParams); for (const [paramKey, paramValue] of serializedParams) urlWithQuery.searchParams.append(paramKey, paramValue); const fetchOptions = this.buildFetchOptions(bodyParams); if (options?.dryRun) return { url: urlWithQuery.toString(), method: this.method, headers: fetchOptions.headers, body: fetchOptions.body instanceof FormData ? "[FormData]" : fetchOptions.body, mappedParams: params }; const response = await fetch(urlWithQuery.toString(), fetchOptions); if (!response.ok) { const responseBody = await response.json().catch(() => null); throw new StackOneAPIError(`API request failed with status ${response.status} for ${url}`, response.status, responseBody, bodyParams); } return await response.json(); } }; //#endregion export { RequestBuilder }; //# sourceMappingURL=requestBuilder.js.map