import crypto from 'crypto'; import { getApiBaseUrl, getAuthorizeBaseUrl, getScopes, getScopesString, getTokenExpiryDate, isAccessTokenExpired, getRequiredAuthConfig, getBasicAuthHeader, logger, } from './shared.js'; const TOKEN_PATH = '/identity/v1/oauth2/token'; async function mintToken(marketplace, body) { const response = await fetch(`${getApiBaseUrl(marketplace)}${TOKEN_PATH}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: getBasicAuthHeader(marketplace), }, body: new URLSearchParams(body).toString(), }); const data = await response.json(); if (!response.ok || data.error) { const message = data.error_description || data.error || response.statusText; logger.error(`eBay token request failed: ${message}`); throw new Error(`eBay token request failed: ${message}`); } return data; } export function createAuthorizationUrl(marketplace, { state } = {}) { const { clientId } = getRequiredAuthConfig(marketplace); const { ruName, locale, prompt } = marketplace.config || {}; if (!ruName) { throw new Error('eBay marketplace is missing required config (ruName)'); } const url = new URL('/oauth2/authorize', getAuthorizeBaseUrl(marketplace)); url.searchParams.set('client_id', clientId); url.searchParams.set('redirect_uri', ruName); url.searchParams.set('response_type', 'code'); url.searchParams.set('scope', getScopesString(marketplace)); if (state) { url.searchParams.set('state', state); } if (locale) { url.searchParams.set('locale', locale); } if (prompt) { url.searchParams.set('prompt', prompt); } return url.toString(); } export async function exchangeAuthorizationCode(marketplace, { code }) { const { ruName } = marketplace.config || {}; if (!code) { throw new Error('Missing eBay authorization code'); } if (!ruName) { throw new Error('eBay marketplace is missing required config (ruName)'); } const tokenData = await mintToken(marketplace, { grant_type: 'authorization_code', code, redirect_uri: ruName, }); return { configUpdates: { accessToken: tokenData.access_token, accessTokenExpiresAt: getTokenExpiryDate(tokenData.expires_in).toISOString(), refreshToken: tokenData.refresh_token || marketplace.config.refreshToken, scopes: getScopes(marketplace), tokenType: tokenData.token_type, }, marketplaceUpdates: { connected: true, connectedAt: new Date(), }, data: { expiresIn: tokenData.expires_in, tokenType: tokenData.token_type, }, }; } export async function refreshAuth(marketplace) { const { refreshToken } = marketplace.config || {}; if (!refreshToken) { throw new Error('eBay marketplace is missing required config (refreshToken)'); } const tokenData = await mintToken(marketplace, { grant_type: 'refresh_token', refresh_token: refreshToken, scope: getScopesString(marketplace), }); return { configUpdates: { accessToken: tokenData.access_token, accessTokenExpiresAt: getTokenExpiryDate(tokenData.expires_in).toISOString(), refreshToken: tokenData.refresh_token || refreshToken, scopes: getScopes(marketplace), tokenType: tokenData.token_type, lastTokenRefreshAt: new Date().toISOString(), }, data: { expiresIn: tokenData.expires_in, tokenType: tokenData.token_type, }, }; } export async function ensureAuthenticatedMarketplace(marketplace) { if (!isAccessTokenExpired(marketplace)) { return { marketplace }; } const authResult = await refreshAuth(marketplace); return { marketplace: { ...marketplace, config: { ...(marketplace.config || {}), ...authResult.configUpdates, }, }, configUpdates: authResult.configUpdates, }; } export function canVerifyWebhookSignature(marketplace) { return !!marketplace.config?.verificationToken; } export function verifyWebhookSignature(marketplace, rawBody, signature) { const verificationToken = marketplace.config?.verificationToken; if (!verificationToken) { return false; } const hash = crypto .createHash('sha256') .update(rawBody + verificationToken) .digest('base64'); try { return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature)); } catch { return false; } }