Files
vendoo/lib/etsy-api.mjs
T
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

179 lines
5.2 KiB
JavaScript

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)}`);
}