/** * Squarespace API Client * Comprehensive client with OAuth2, pagination, error handling, and retry logic */ import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; import type { SquarespaceConfig, OAuthTokenResponse, PaginatedResponse, PaginationParams, // Products Product, CreateProductRequest, UpdateProductRequest, GetProductsResponse, ProductVariant, ProductImage, // Orders Order, CreateOrderRequest, FulfillOrderRequest, OrdersQueryParams, GetOrdersResponse, // Inventory InventoryItem, UpdateInventoryRequest, // Transactions Transaction, CreateRefundRequest, // Profiles Profile, ProfilesQueryParams, GetProfilesResponse, // Webhooks WebhookSubscription, CreateWebhookRequest, UpdateWebhookRequest, WebhookTestNotification, GetWebhooksResponse, // Site & Pages Website, Collection, Page, CreatePageRequest, UpdatePageRequest, // Forms Form, FormSubmission, FormSubmissionsQueryParams, GetFormSubmissionsResponse, // Blog BlogCollection, BlogPost, CreateBlogPostRequest, UpdateBlogPostRequest, GetBlogPostsResponse, // Analytics AnalyticsParams, RevenueMetrics, ProductPerformance, ConversionMetrics, } from '../types/index.js'; const DEFAULT_BASE_URL = 'https://api.squarespace.com/1.0'; const DEFAULT_TIMEOUT = 30000; const DEFAULT_RETRY_ATTEMPTS = 3; const OAUTH_TOKEN_URL = 'https://login.squarespace.com/api/1/login/oauth/provider/tokens'; export class SquarespaceClient { private client: AxiosInstance; private accessToken: string; private refreshToken?: string; private clientId?: string; private clientSecret?: string; private retryAttempts: number; private tokenExpiresAt?: Date; constructor(config: SquarespaceConfig) { this.accessToken = config.accessToken; this.refreshToken = config.refreshToken; this.clientId = config.clientId; this.clientSecret = config.clientSecret; this.retryAttempts = config.retryAttempts ?? DEFAULT_RETRY_ATTEMPTS; this.client = axios.create({ baseURL: config.baseUrl || DEFAULT_BASE_URL, timeout: config.timeout || DEFAULT_TIMEOUT, headers: { 'User-Agent': 'Squarespace-MCP-Server/1.0', 'Content-Type': 'application/json', }, }); // Request interceptor to add auth and handle token refresh this.client.interceptors.request.use( async (config) => { // Check if token needs refresh if (this.shouldRefreshToken()) { await this.refreshAccessToken(); } config.headers.Authorization = `Bearer ${this.accessToken}`; return config; }, (error) => Promise.reject(error) ); // Response interceptor for error handling this.client.interceptors.response.use( (response) => response, async (error: AxiosError) => { if (error.response?.status === 401 && this.refreshToken) { // Token expired, try to refresh try { await this.refreshAccessToken(); // Retry the original request const config = error.config; if (config) { config.headers.Authorization = `Bearer ${this.accessToken}`; return this.client.request(config); } } catch (refreshError) { return Promise.reject(this.handleError(refreshError as AxiosError)); } } return Promise.reject(this.handleError(error)); } ); } /** * Check if access token should be refreshed */ private shouldRefreshToken(): boolean { if (!this.tokenExpiresAt || !this.refreshToken) { return false; } // Refresh if less than 5 minutes remaining const fiveMinutes = 5 * 60 * 1000; return Date.now() >= this.tokenExpiresAt.getTime() - fiveMinutes; } /** * Refresh the access token using refresh token */ private async refreshAccessToken(): Promise { if (!this.refreshToken || !this.clientId || !this.clientSecret) { throw new Error('Missing refresh token or OAuth credentials'); } const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); try { const response = await axios.post( OAUTH_TOKEN_URL, { grant_type: 'refresh_token', refresh_token: this.refreshToken, }, { headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json', 'User-Agent': 'Squarespace-MCP-Server/1.0', }, } ); this.accessToken = response.data.access_token; if (response.data.refresh_token) { this.refreshToken = response.data.refresh_token; } // Set expiration time (30 minutes from now) this.tokenExpiresAt = new Date(Date.now() + 30 * 60 * 1000); } catch (error) { throw new Error('Failed to refresh access token'); } } /** * Handle API errors and convert to SquarespaceAPIError */ private handleError(error: AxiosError): SquarespaceAPIError { const response = error.response; const status = response?.status || 500; const data = response?.data as any; return new SquarespaceAPIError( status, data?.type || 'UNKNOWN_ERROR', data?.message || error.message || 'An unknown error occurred', data?.errors ); } /** * Make a request with automatic retry logic */ private async makeRequest( config: AxiosRequestConfig, attempt: number = 0 ): Promise { try { const response = await this.client.request(config); return response.data; } catch (error) { const axiosError = error as AxiosError; // Retry on 429 (rate limit) or 5xx errors if ( attempt < this.retryAttempts && (axiosError.response?.status === 429 || (axiosError.response?.status && axiosError.response.status >= 500)) ) { // Exponential backoff: 1s, 2s, 4s, etc. const delay = Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); return this.makeRequest(config, attempt + 1); } throw error; } } // ============================================================================ // Products API // ============================================================================ async getProducts(params?: PaginationParams): Promise { return this.makeRequest({ method: 'GET', url: '/commerce/products', params, }); } async getProduct(productId: string): Promise { return this.makeRequest({ method: 'GET', url: `/commerce/products/${productId}`, }); } async createProduct(product: CreateProductRequest): Promise { return this.makeRequest({ method: 'POST', url: '/commerce/products', data: product, }); } async updateProduct(productId: string, updates: UpdateProductRequest): Promise { return this.makeRequest({ method: 'PUT', url: `/commerce/products/${productId}`, data: updates, }); } async deleteProduct(productId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/commerce/products/${productId}`, }); } async createProductVariant(productId: string, variant: any): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/products/${productId}/variants`, data: variant, }); } async updateProductVariant( productId: string, variantId: string, updates: any ): Promise { return this.makeRequest({ method: 'PUT', url: `/commerce/products/${productId}/variants/${variantId}`, data: updates, }); } async deleteProductVariant(productId: string, variantId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/commerce/products/${productId}/variants/${variantId}`, }); } async uploadProductImage(productId: string, imageFile: Buffer): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/products/${productId}/images`, data: imageFile, headers: { 'Content-Type': 'image/jpeg', }, }); } async deleteProductImage(productId: string, imageId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/commerce/products/${productId}/images/${imageId}`, }); } async reorderProductImage(productId: string, imageId: string, position: number): Promise { await this.makeRequest({ method: 'POST', url: `/commerce/products/${productId}/images/${imageId}/reorder`, data: { position }, }); } // ============================================================================ // Orders API // ============================================================================ async getOrders(params?: OrdersQueryParams): Promise { return this.makeRequest({ method: 'GET', url: '/commerce/orders', params, }); } async getOrder(orderId: string): Promise { return this.makeRequest({ method: 'GET', url: `/commerce/orders/${orderId}`, }); } async createOrder(order: CreateOrderRequest): Promise { return this.makeRequest({ method: 'POST', url: '/commerce/orders', data: order, }); } async fulfillOrder(orderId: string, fulfillment: FulfillOrderRequest): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/orders/${orderId}/fulfillments`, data: fulfillment, }); } async addOrderNote(orderId: string, note: string): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/orders/${orderId}/notes`, data: { text: note }, }); } // ============================================================================ // Inventory API // ============================================================================ async getInventory(variantId: string): Promise { return this.makeRequest({ method: 'GET', url: `/commerce/inventory/${variantId}`, }); } async updateInventory(variantId: string, updates: UpdateInventoryRequest): Promise { return this.makeRequest({ method: 'PUT', url: `/commerce/inventory/${variantId}`, data: updates, }); } async adjustInventory(variantId: string, adjustment: number): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/inventory/${variantId}/adjust`, data: { adjustment }, }); } // ============================================================================ // Transactions API // ============================================================================ async getTransactions(orderId: string): Promise { const response = await this.makeRequest<{ transactions: Transaction[] }>({ method: 'GET', url: `/commerce/orders/${orderId}/transactions`, }); return response.transactions; } async createRefund(orderId: string, refund: CreateRefundRequest): Promise { return this.makeRequest({ method: 'POST', url: `/commerce/orders/${orderId}/refunds`, data: refund, }); } // ============================================================================ // Profiles API (Customers, Subscribers, Donors) // ============================================================================ async getProfiles(params?: ProfilesQueryParams): Promise { return this.makeRequest({ method: 'GET', url: '/profiles', params, }); } async getProfile(profileId: string): Promise { return this.makeRequest({ method: 'GET', url: `/profiles/${profileId}`, }); } async getCustomers(params?: ProfilesQueryParams): Promise { return this.makeRequest({ method: 'GET', url: '/profiles', params: { ...params, type: 'CUSTOMER' }, }); } async getSubscribers(params?: ProfilesQueryParams): Promise { return this.makeRequest({ method: 'GET', url: '/profiles', params: { ...params, type: 'SUBSCRIBER' }, }); } async getDonors(params?: ProfilesQueryParams): Promise { return this.makeRequest({ method: 'GET', url: '/profiles', params: { ...params, type: 'DONOR' }, }); } // ============================================================================ // Webhooks API // ============================================================================ async getWebhooks(): Promise { const response = await this.makeRequest({ method: 'GET', url: '/webhook_subscriptions', }); return response.webhooks; } async getWebhook(webhookId: string): Promise { return this.makeRequest({ method: 'GET', url: `/webhook_subscriptions/${webhookId}`, }); } async createWebhook(webhook: CreateWebhookRequest): Promise { return this.makeRequest({ method: 'POST', url: '/webhook_subscriptions', data: webhook, }); } async updateWebhook( webhookId: string, updates: UpdateWebhookRequest ): Promise { return this.makeRequest({ method: 'PUT', url: `/webhook_subscriptions/${webhookId}`, data: updates, }); } async deleteWebhook(webhookId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/webhook_subscriptions/${webhookId}`, }); } async sendWebhookTest(notification: WebhookTestNotification): Promise { await this.makeRequest({ method: 'POST', url: `/webhook_subscriptions/${notification.webhookId}/test`, data: { topic: notification.topic }, }); } async rotateWebhookSecret(webhookId: string): Promise { return this.makeRequest({ method: 'POST', url: `/webhook_subscriptions/${webhookId}/rotate_secret`, }); } // ============================================================================ // Website & Pages API // ============================================================================ async getWebsite(): Promise { return this.makeRequest({ method: 'GET', url: '/website', }); } async getCollections(): Promise { const response = await this.makeRequest<{ collections: Collection[] }>({ method: 'GET', url: '/website/collections', }); return response.collections; } async getCollection(collectionId: string): Promise { return this.makeRequest({ method: 'GET', url: `/website/collections/${collectionId}`, }); } async getPages(collectionId?: string): Promise { const url = collectionId ? `/website/collections/${collectionId}/pages` : '/website/pages'; const response = await this.makeRequest<{ pages: Page[] }>({ method: 'GET', url, }); return response.pages; } async getPage(pageId: string): Promise { return this.makeRequest({ method: 'GET', url: `/website/pages/${pageId}`, }); } async createPage(page: CreatePageRequest): Promise { return this.makeRequest({ method: 'POST', url: `/website/collections/${page.collectionId}/pages`, data: page, }); } async updatePage(pageId: string, updates: UpdatePageRequest): Promise { return this.makeRequest({ method: 'PUT', url: `/website/pages/${pageId}`, data: updates, }); } async deletePage(pageId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/website/pages/${pageId}`, }); } // ============================================================================ // Forms API // ============================================================================ async getForms(): Promise { const response = await this.makeRequest<{ forms: Form[] }>({ method: 'GET', url: '/forms', }); return response.forms; } async getForm(formId: string): Promise
{ return this.makeRequest({ method: 'GET', url: `/forms/${formId}`, }); } async getFormSubmissions( formId?: string, params?: FormSubmissionsQueryParams ): Promise { const url = formId ? `/forms/${formId}/submissions` : '/form_submissions'; return this.makeRequest({ method: 'GET', url, params, }); } async getFormSubmission(submissionId: string): Promise { return this.makeRequest({ method: 'GET', url: `/form_submissions/${submissionId}`, }); } // ============================================================================ // Blog API // ============================================================================ async getBlogs(): Promise { const response = await this.makeRequest<{ blogs: BlogCollection[] }>({ method: 'GET', url: '/blogs', }); return response.blogs; } async getBlog(blogId: string): Promise { return this.makeRequest({ method: 'GET', url: `/blogs/${blogId}`, }); } async getBlogPosts(blogId: string, params?: PaginationParams): Promise { return this.makeRequest({ method: 'GET', url: `/blogs/${blogId}/posts`, params, }); } async getBlogPost(blogId: string, postId: string): Promise { return this.makeRequest({ method: 'GET', url: `/blogs/${blogId}/posts/${postId}`, }); } async createBlogPost(blogId: string, post: CreateBlogPostRequest): Promise { return this.makeRequest({ method: 'POST', url: `/blogs/${blogId}/posts`, data: post, }); } async updateBlogPost( blogId: string, postId: string, updates: UpdateBlogPostRequest ): Promise { return this.makeRequest({ method: 'PUT', url: `/blogs/${blogId}/posts/${postId}`, data: updates, }); } async deleteBlogPost(blogId: string, postId: string): Promise { await this.makeRequest({ method: 'DELETE', url: `/blogs/${blogId}/posts/${postId}`, }); } async publishBlogPost(blogId: string, postId: string): Promise { return this.makeRequest({ method: 'POST', url: `/blogs/${blogId}/posts/${postId}/publish`, }); } async unpublishBlogPost(blogId: string, postId: string): Promise { return this.makeRequest({ method: 'POST', url: `/blogs/${blogId}/posts/${postId}/unpublish`, }); } // ============================================================================ // Analytics API (Custom/Extended) // ============================================================================ async getRevenueMetrics(params: AnalyticsParams): Promise { // This would need to be built from orders data const orders = await this.getOrders({ modifiedAfter: params.startDate, modifiedBefore: params.endDate, }); const totalRevenue = orders.result.reduce( (sum, order) => sum + parseFloat(order.grandTotal.value), 0 ); return { totalRevenue: { value: totalRevenue.toFixed(2), currency: orders.result[0]?.grandTotal.currency || 'USD', }, orderCount: orders.result.length, averageOrderValue: { value: orders.result.length > 0 ? (totalRevenue / orders.result.length).toFixed(2) : '0', currency: orders.result[0]?.grandTotal.currency || 'USD', }, period: { start: params.startDate, end: params.endDate, }, }; } async getTopProducts(params: AnalyticsParams, limit: number = 10): Promise { // Build from orders data const orders = await this.getOrders({ modifiedAfter: params.startDate, modifiedBefore: params.endDate, }); const productStats = new Map(); orders.result.forEach(order => { order.lineItems.forEach(item => { const existing = productStats.get(item.productId) || { name: item.productName, units: 0, revenue: 0, currency: item.lineItemPricePaid.currency, }; existing.units += item.quantity; existing.revenue += parseFloat(item.lineItemPricePaid.value); productStats.set(item.productId, existing); }); }); return Array.from(productStats.entries()) .map(([productId, stats]) => ({ productId, productName: stats.name, unitsSold: stats.units, revenue: { value: stats.revenue.toFixed(2), currency: stats.currency, }, })) .sort((a, b) => parseFloat(b.revenue.value) - parseFloat(a.revenue.value)) .slice(0, limit); } } export class SquarespaceAPIError extends Error { constructor( public status: number, public type: string, message: string, public errors?: Array<{ field?: string; message: string }> ) { super(message); this.name = 'SquarespaceAPIError'; } }