UNPKG

@proveanything/smartlinks

Version:

Official JavaScript/TypeScript SDK for the Smartlinks API

307 lines (306 loc) 11.3 kB
// src/http.ts // This module replaces the ApiClient constructor. It keeps baseURL, apiKey, bearerToken // in module-scope variables, and provides a shared `request<T>(path)` helper that will // be used by all namespaced files (collection.ts, product.ts, etc.). let baseURL = null; let apiKey = undefined; let bearerToken = undefined; let proxyMode = false; /** * Call this once (e.g. at app startup) to configure baseURL/auth. * * @param options - Configuration options * @property {string} options.baseURL - The root URL of the Smartlinks API (e.g. "https://smartlinks.app/api/v1") * @property {string} [options.apiKey] - (Optional) API key for X-API-Key header * @property {string} [options.bearerToken] - (Optional) Bearer token for AUTHORIZATION header * @property {boolean} [options.proxyMode] - (Optional) Tells the API that it is running in an iframe via parent proxy */ export function initializeApi(options) { baseURL = options.baseURL.replace(/\/+\$/, ""); // trim trailing slash apiKey = options.apiKey; bearerToken = options.bearerToken; proxyMode = !!options.proxyMode; } /** * Allows setting the bearerToken at runtime (e.g. after login/logout). */ export function setBearerToken(token) { bearerToken = token; } // Map of pending proxy requests: id -> {resolve, reject} const proxyPending = {}; function generateProxyId() { return "proxy_" + Math.random().toString(36).slice(2) + Date.now(); } // Shared listener for proxy responses function ensureProxyListener() { if (window._smartlinksProxyListener) return; window.addEventListener("message", (event) => { const msg = event.data; if (!msg || !msg._smartlinksProxyResponse || !msg.id) return; const pending = proxyPending[msg.id]; if (pending) { if (msg.error) { pending.reject(new Error(msg.error)); } else { pending.resolve(msg.data); } delete proxyPending[msg.id]; } }); window._smartlinksProxyListener = true; } // Proxy request implementation async function proxyRequest(method, path, body, headers, options) { ensureProxyListener(); const id = generateProxyId(); const msg = { _smartlinksProxyRequest: true, id, method, path, body, headers, options, }; return new Promise((resolve, reject) => { proxyPending[id] = { resolve, reject }; window.parent.postMessage(msg, "*"); // Optionally: add a timeout here to reject if no response }); } /** * Internal helper that performs a GET request to \`\${baseURL}\${path}\`, * injecting headers for apiKey or bearerToken if present. * Returns the parsed JSON as T, or throws an Error. */ export async function request(path) { if (proxyMode) { return proxyRequest("GET", path); } if (!baseURL) { throw new Error("HTTP client is not initialized. Call initializeApi(...) first."); } const url = `${baseURL}${path}`; const headers = { "Content-Type": "application/json", }; if (apiKey) { headers["X-API-Key"] = apiKey; } if (bearerToken) { headers["AUTHORIZATION"] = `Bearer ${bearerToken}`; } const response = await fetch(url, { method: "GET", headers, }); if (!response.ok) { // Try to parse ErrorResponse; if that fails, throw generic try { const errBody = (await response.json()); throw new Error(`Error ${errBody.code}: ${errBody.message}`); } catch (_a) { throw new Error(`Request to ${url} failed with status ${response.status}`); } } return (await response.json()); } /** * Internal helper that performs a POST request to `${baseURL}${path}`, * injecting headers for apiKey or bearerToken if present. * If body is FormData, Content-Type is not set. * Returns the parsed JSON as T, or throws an Error. */ export async function post(path, body, extraHeaders) { if (proxyMode) { return proxyRequest("POST", path, body, extraHeaders); } if (!baseURL) { throw new Error("HTTP client is not initialized. Call initializeApi(...) first."); } const url = `${baseURL}${path}`; const headers = extraHeaders ? Object.assign({}, extraHeaders) : {}; if (apiKey) headers["X-API-Key"] = apiKey; if (bearerToken) headers["AUTHORIZATION"] = `Bearer ${bearerToken}`; // Only set Content-Type for non-FormData bodies if (!(body instanceof FormData)) { headers["Content-Type"] = "application/json"; } const response = await fetch(url, { method: "POST", headers, body: body instanceof FormData ? body : JSON.stringify(body), }); if (!response.ok) { try { const errBody = (await response.json()); throw new Error(`Error ${errBody.code}: ${errBody.message}`); } catch (_a) { throw new Error(`Request to ${url} failed with status ${response.status}`); } } return (await response.json()); } /** * Internal helper that performs a PUT request to `${baseURL}${path}`, * injecting headers for apiKey or bearerToken if present. * If body is FormData, Content-Type is not set. * Returns the parsed JSON as T, or throws an Error. */ export async function put(path, body, extraHeaders) { if (proxyMode) { return proxyRequest("PUT", path, body, extraHeaders); } if (!baseURL) { throw new Error("HTTP client is not initialized. Call initializeApi(...) first."); } const url = `${baseURL}${path}`; const headers = extraHeaders ? Object.assign({}, extraHeaders) : {}; if (apiKey) headers["X-API-Key"] = apiKey; if (bearerToken) headers["AUTHORIZATION"] = `Bearer ${bearerToken}`; // Only set Content-Type for non-FormData bodies if (!(body instanceof FormData)) { headers["Content-Type"] = "application/json"; } const response = await fetch(url, { method: "PUT", headers, body: body instanceof FormData ? body : JSON.stringify(body), }); if (!response.ok) { try { const errBody = (await response.json()); throw new Error(`Error ${errBody.code}: ${errBody.message}`); } catch (_a) { throw new Error(`Request to ${url} failed with status ${response.status}`); } } return (await response.json()); } /** * Internal helper that performs a request to `${baseURL}${path}` with custom options, * injecting headers for apiKey or bearerToken if present. * Returns the parsed JSON as T, or throws an Error. */ export async function requestWithOptions(path, options) { if (proxyMode) { return proxyRequest(options.method || "GET", path, options.body, options.headers, options); } if (!baseURL) { throw new Error("HTTP client is not initialized. Call initializeApi(...) first."); } const url = `${baseURL}${path}`; // Safely merge headers, converting Headers/init to Record<string, string> let extraHeaders = {}; if (options.headers) { if (options.headers instanceof Headers) { options.headers.forEach((value, key) => { extraHeaders[key] = value; }); } else if (Array.isArray(options.headers)) { for (const [key, value] of options.headers) { extraHeaders[key] = value; } } else { extraHeaders = Object.assign({}, options.headers); } } const headers = Object.assign(Object.assign(Object.assign({ "Content-Type": "application/json" }, (apiKey ? { "X-API-Key": apiKey } : {})), (bearerToken ? { "AUTHORIZATION": `Bearer ${bearerToken}` } : {})), extraHeaders); const response = await fetch(url, Object.assign(Object.assign({}, options), { headers })); if (!response.ok) { try { const errBody = (await response.json()); throw new Error(`Error ${errBody.code}: ${errBody.message}`); } catch (_a) { throw new Error(`Request to ${url} failed with status ${response.status}`); } } return (await response.json()); } /** * Internal helper that performs a DELETE request to `${baseURL}${path}`, * injecting headers for apiKey or bearerToken if present. * Returns the parsed JSON as T, or throws an Error. */ export async function del(path, extraHeaders) { if (proxyMode) { return proxyRequest("DELETE", path, undefined, extraHeaders); } if (!baseURL) { throw new Error("HTTP client is not initialized. Call initializeApi(...) first."); } const url = `${baseURL}${path}`; const headers = extraHeaders ? Object.assign({}, extraHeaders) : {}; if (apiKey) headers["X-API-Key"] = apiKey; if (bearerToken) headers["AUTHORIZATION"] = `Bearer ${bearerToken}`; const response = await fetch(url, { method: "DELETE", headers, }); if (!response.ok) { try { const errBody = (await response.json()); throw new Error(`Error ${errBody.code}: ${errBody.message}`); } catch (_a) { throw new Error(`Request to ${url} failed with status ${response.status}`); } } // If the response is empty, just return undefined if (response.status === 204) return undefined; return (await response.json()); } /** * Returns the common headers used for API requests, including apiKey and bearerToken if set. */ export function getApiHeaders() { const headers = {}; if (apiKey) headers["X-API-Key"] = apiKey; if (bearerToken) headers["AUTHORIZATION"] = `Bearer ${bearerToken}`; return headers; } /** * Sends a custom proxy message to the parent Smartlinks application when running in an iframe. * This function is used to communicate with the parent window when the SDK is embedded in an iframe * and proxyMode is enabled. It sends a message to the parent and waits for a response. * @param request - The request type string to identify the message type * @param params - The parameters object containing data to send to the parent * @returns The data from the proxy response */ export async function sendCustomProxyMessage(request, params) { if (!proxyMode) { throw new Error("sendCustomProxyMessage can only be used in proxyMode"); } ensureProxyListener(); const id = generateProxyId(); const msg = { _smartlinksCustomProxyRequest: true, id, request, params, }; return new Promise((resolve, reject) => { proxyPending[id] = { resolve, reject }; window.parent.postMessage(msg, "*"); // Optionally: add a timeout here to reject if no response }); }