import { randomBytes, createHash } from 'crypto'; import { setSecret } from './secret-store.mjs'; const ETSY_BASE = 'https://api.etsy.com/v3'; const ETSY_AUTH = 'https://www.etsy.com/oauth/connect'; const ETSY_TOKEN = 'https://api.etsy.com/v3/public/oauth/token'; let pkceStore = {}; function setEnvVar(key, value) { setSecret(key, value, { source: 'oauth' }); } function generatePKCE() { const verifier = randomBytes(32).toString('base64url'); const challenge = createHash('sha256').update(verifier).digest('base64url'); return { verifier, challenge }; } export function getAuthUrl(redirectUri) { const apiKey = process.env.ETSY_API_KEY; if (!apiKey) throw new Error('ETSY_API_KEY nicht gesetzt'); const { verifier, challenge } = generatePKCE(); const state = randomBytes(16).toString('hex'); pkceStore[state] = verifier; const params = new URLSearchParams({ response_type: 'code', client_id: apiKey, redirect_uri: redirectUri, scope: 'listings_w listings_r shops_r images_w', state, code_challenge: challenge, code_challenge_method: 'S256', }); return `${ETSY_AUTH}?${params}`; } export async function handleCallback(code, state, redirectUri) { const verifier = pkceStore[state]; if (!verifier) throw new Error('Ungültiger OAuth-State'); delete pkceStore[state]; const apiKey = process.env.ETSY_API_KEY; const res = await fetch(ETSY_TOKEN, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: apiKey, redirect_uri: redirectUri, code, code_verifier: verifier, }), }); if (!res.ok) { const err = await res.text(); throw new Error(`Etsy Token-Fehler: ${err}`); } const tokens = await res.json(); setEnvVar('ETSY_ACCESS_TOKEN', tokens.access_token); setEnvVar('ETSY_REFRESH_TOKEN', tokens.refresh_token); return tokens; } async function refreshToken() { const apiKey = process.env.ETSY_API_KEY; const refreshToken = process.env.ETSY_REFRESH_TOKEN; if (!refreshToken) throw new Error('Kein Etsy Refresh-Token'); const res = await fetch(ETSY_TOKEN, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: apiKey, refresh_token: refreshToken, }), }); if (!res.ok) throw new Error('Etsy Token-Refresh fehlgeschlagen'); const tokens = await res.json(); setEnvVar('ETSY_ACCESS_TOKEN', tokens.access_token); setEnvVar('ETSY_REFRESH_TOKEN', tokens.refresh_token); return tokens.access_token; } async function etsyFetch(path, opts = {}) { let token = process.env.ETSY_ACCESS_TOKEN; if (!token) throw new Error('Nicht mit Etsy verbunden'); const apiKey = process.env.ETSY_API_KEY; const headers = { 'x-api-key': apiKey, Authorization: `Bearer ${token}`, ...opts.headers }; let res = await fetch(`${ETSY_BASE}${path}`, { ...opts, headers }); if (res.status === 401) { token = await refreshToken(); headers.Authorization = `Bearer ${token}`; res = await fetch(`${ETSY_BASE}${path}`, { ...opts, headers }); } if (!res.ok) { const err = await res.text(); throw new Error(`Etsy API Fehler (${res.status}): ${err}`); } return res.json(); } export async function getShopId() { const data = await etsyFetch('/application/users/me'); const userId = data.user_id; const shop = await etsyFetch(`/application/users/${userId}/shops`); if (!shop.results?.length) throw new Error('Kein Etsy-Shop gefunden'); return shop.results[0].shop_id; } export async function createEtsyListing(listing, shopId) { const tags = Array.isArray(listing.tags) ? listing.tags.slice(0, 13) : []; const price = listing.price || listing.suggested_price || 10; const body = { quantity: 1, title: listing.title, description: listing.description, price: price, who_made: 'someone_else', when_made: '2020_2025', taxonomy_id: 1, tags, type: 'physical', shipping_profile_id: null, state: 'draft', }; const result = await etsyFetch(`/application/shops/${shopId}/listings`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return result; } export async function uploadEtsyImage(shopId, listingId, imageBuffer, filename) { const formData = new FormData(); formData.append('image', new Blob([imageBuffer]), filename); const token = process.env.ETSY_ACCESS_TOKEN; const apiKey = process.env.ETSY_API_KEY; const res = await fetch(`${ETSY_BASE}/application/shops/${shopId}/listings/${listingId}/images`, { method: 'POST', headers: { 'x-api-key': apiKey, Authorization: `Bearer ${token}`, }, body: formData, }); if (!res.ok) { const err = await res.text(); throw new Error(`Bild-Upload fehlgeschlagen: ${err}`); } return res.json(); } export function isConnected() { return !!(process.env.ETSY_ACCESS_TOKEN && process.env.ETSY_API_KEY); } export async function getEtsyListing(listingId) { if (!listingId) throw new Error('Etsy Listing-ID fehlt'); return etsyFetch(`/application/listings/${encodeURIComponent(listingId)}`); }