import config from '../../../config.js'; import log4js from 'log4js'; const logger = log4js.getLogger('eBay'); logger.level = config.server.logLevel; const DEBUG_PAYLOAD_MAX_LENGTH = 4000; function formatDebugPayload(value, { maxLength = DEBUG_PAYLOAD_MAX_LENGTH } = {}) { if (value === null || value === undefined) { return value; } let text; try { text = typeof value === 'string' ? value : JSON.stringify(value); } catch { text = String(value); } if (text.length <= maxLength) { return text; } return `${text.slice(0, maxLength)}... [truncated ${text.length - maxLength} chars]`; } export function getEbayMarketplaceId(marketplace) { return marketplace?.config?.marketplaceId || 'EBAY_GB'; } function getMarketplaceDebugContext(marketplace) { return { marketplace: marketplace?.name, marketplaceId: getEbayMarketplaceId(marketplace), sandbox: marketplace?.config?.sandbox ?? false, }; } const SANDBOX_API_URL = 'https://api.sandbox.ebay.com'; const PRODUCTION_API_URL = 'https://api.ebay.com'; const SANDBOX_MEDIA_API_URL = 'https://apim.sandbox.ebay.com'; const PRODUCTION_MEDIA_API_URL = 'https://apim.ebay.com'; const SANDBOX_AUTH_URL = 'https://auth.sandbox.ebay.com'; const PRODUCTION_AUTH_URL = 'https://auth.ebay.com'; const TOKEN_PATH = '/identity/v1/oauth2/token'; const DEFAULT_SCOPES = [ 'https://api.ebay.com/oauth/api_scope', 'https://api.ebay.com/oauth/api_scope/sell.inventory', 'https://api.ebay.com/oauth/api_scope/sell.fulfillment', 'https://api.ebay.com/oauth/api_scope/sell.account', 'https://api.ebay.com/oauth/api_scope/commerce.notification.subscription', 'https://api.ebay.com/oauth/api_scope/commerce.media', ]; const MARKETPLACE_LANGUAGE_MAP = { EBAY_US: 'en-US', EBAY_GB: 'en-GB', EBAY_AU: 'en-AU', EBAY_CA: 'en-CA', EBAY_DE: 'de-DE', EBAY_FR: 'fr-FR', EBAY_ES: 'es-ES', EBAY_IT: 'it-IT', EBAY_NL: 'nl-NL', EBAY_BE: 'nl-BE', }; export function getApiBaseUrl(marketplace) { return marketplace.config.sandbox ? SANDBOX_API_URL : PRODUCTION_API_URL; } export function getMediaApiBaseUrl(marketplace) { return marketplace.config.sandbox ? SANDBOX_MEDIA_API_URL : PRODUCTION_MEDIA_API_URL; } export function getAuthorizeBaseUrl(marketplace) { return marketplace.config.sandbox ? SANDBOX_AUTH_URL : PRODUCTION_AUTH_URL; } export function getScopes(marketplace) { if (Array.isArray(marketplace.config.scopes) && marketplace.config.scopes.length) { return marketplace.config.scopes; } if (typeof marketplace.config.scopes === 'string' && marketplace.config.scopes.trim()) { return marketplace.config.scopes.trim().split(/\s+/); } return DEFAULT_SCOPES; } export function getScopesString(marketplace) { return getScopes(marketplace).join(' '); } function isValidLanguageTag(value) { return /^[a-z]{2,3}(?:-[A-Z]{2})?$/.test(value || ''); } export function getAcceptLanguage(marketplace) { const configured = marketplace.config?.acceptLanguage || marketplace.config?.locale || MARKETPLACE_LANGUAGE_MAP[marketplace.config?.marketplaceId]; if (typeof configured === 'string') { const normalized = configured.replace('_', '-').trim(); if (isValidLanguageTag(normalized)) { return normalized; } } return 'en-GB'; } export function getTokenExpiryDate(expiresInSeconds) { return new Date(Date.now() + Math.max(Number(expiresInSeconds || 0) - 60, 0) * 1000); } export function isAccessTokenExpired(marketplace) { const { accessToken, accessTokenExpiresAt } = marketplace.config || {}; if (!accessToken || !accessTokenExpiresAt) { return true; } return new Date(accessTokenExpiresAt).getTime() <= Date.now(); } export function getRequiredAuthConfig(marketplace) { const { clientId, clientSecret } = marketplace.config || {}; if (!clientId || !clientSecret) { throw new Error('eBay marketplace is missing required config (clientId, clientSecret)'); } return { clientId, clientSecret }; } export function getBasicAuthHeader(marketplace) { const { clientId, clientSecret } = getRequiredAuthConfig(marketplace); return `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`; } export async function makeRequest({ marketplace, method = 'GET', path, params = {}, body = null, acceptableStatuses = [], extraHeaders = {}, logResponse = true, rawBody = false, contentType, baseUrl, } = {}) { const { accessToken } = marketplace.config || {}; if (!accessToken) { throw new Error( 'eBay marketplace is not authenticated. Complete marketplace authorization first.' ); } const queryString = Object.entries(params) .filter(([, value]) => value !== undefined && value !== null && value !== '') .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); const apiBase = baseUrl || getApiBaseUrl(marketplace); const url = queryString ? `${apiBase}${path}?${queryString}` : `${apiBase}${path}`; const headers = { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', 'Accept-Language': getAcceptLanguage(marketplace), ...extraHeaders, }; headers['X-EBAY-C-MARKETPLACE-ID'] = getEbayMarketplaceId(marketplace); const fetchOptions = { method, headers, }; const isRawBody = rawBody === true || Boolean(contentType); if (body != null && method !== 'GET') { if (isRawBody) { if (contentType) { fetchOptions.headers['Content-Type'] = contentType; } fetchOptions.body = body; } else { fetchOptions.headers['Content-Type'] = 'application/json'; fetchOptions.headers['Content-Language'] = getAcceptLanguage(marketplace); fetchOptions.body = JSON.stringify(body); } } const startedAt = Date.now(); const debugBody = isRawBody ? `[binary ${body?.length ?? 0} bytes]` : body ? formatDebugPayload(body) : undefined; logger.debug(`eBay API ${method} ${path}`, { ...getMarketplaceDebugContext(marketplace), host: apiBase, params: Object.keys(params).length ? params : undefined, body: debugBody, acceptableStatuses: acceptableStatuses.length ? acceptableStatuses : undefined, }); const response = await fetch(url, fetchOptions); const durationMs = Date.now() - startedAt; if (response.status === 204) { logger.debug(`eBay API ${method} ${path} -> 204 No Content (${durationMs}ms)`, { ...getMarketplaceDebugContext(marketplace), }); return null; } const responseText = await response.text(); let data = null; if (responseText) { try { data = JSON.parse(responseText); } catch { data = responseText; } } if (!response.ok && acceptableStatuses.includes(response.status)) { logger.debug( `eBay API ${method} ${path} -> ${response.status} (acceptable) (${durationMs}ms)`, { ...getMarketplaceDebugContext(marketplace), response: data ? formatDebugPayload(data) : undefined, } ); return null; } if (!response.ok) { const message = (typeof data === 'object' && (data?.errors?.[0]?.longMessage || data?.errors?.[0]?.message || data?.error_description)) || (typeof data === 'string' && data) || response.statusText; logger.error(`eBay API error: ${message}`, { status: response.status, path, durationMs, ...getMarketplaceDebugContext(marketplace), response: data ? formatDebugPayload(data) : undefined, }); throw new Error(`eBay API error (${response.status}): ${message}`); } logger.debug(`eBay API ${method} ${path} -> ${response.status} (${durationMs}ms)`, { ...getMarketplaceDebugContext(marketplace), response: logResponse && data ? formatDebugPayload(data) : undefined, }); return data; } export { logger, formatDebugPayload, getMarketplaceDebugContext };