@profplum700/etsy-v3-api-client
Version:
JavaScript/TypeScript client for the Etsy Open API v3 with OAuth 2.0 authentication
3 lines (2 loc) • 17.1 kB
JavaScript
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).EtsyApiClient={})}(this,function(e){"use strict";class t extends Error{constructor(e,t,s){super(e),this._statusCode=t,this._response=s,this.name="EtsyApiError"}get statusCode(){return this._statusCode}get response(){return this._response}}class s extends Error{constructor(e,t){super(e),this._code=t,this.name="EtsyAuthError"}get code(){return this._code}}class r extends Error{constructor(e,t){super(e),this._retryAfter=t,this.name="EtsyRateLimitError"}get retryAfter(){return this._retryAfter}}const i="undefined"!=typeof window&&void 0!==window.document,n=!1,o="function"==typeof globalThis.importScripts&&"undefined"!=typeof navigator,a="undefined"!=typeof fetch,c="undefined"!=typeof crypto&&void 0!==crypto.subtle,h=(()=>{try{return"undefined"!=typeof localStorage}catch{return!1}})(),u=(()=>{try{return"undefined"!=typeof sessionStorage}catch{return!1}})();function l(){if(!a)throw new Error("Fetch API is not available. Please use Node.js 18+ or a modern browser.")}class g{constructor(){this.tokens=null}async save(e){this.tokens={...e}}async load(){return this.tokens?{...this.tokens}:null}async clear(){this.tokens=null}}class d{constructor(e,t){this.currentTokens=null,this.keystring=e.keystring,this.refreshCallback=e.refreshSave,this.storage=t,this.currentTokens={access_token:e.accessToken,refresh_token:e.refreshToken,expires_at:e.expiresAt,token_type:"Bearer",scope:""}}async getAccessToken(){if(!this.currentTokens&&(this.storage&&(this.currentTokens=await this.storage.load()),!this.currentTokens))throw new s("No tokens available","NO_TOKENS");const e=new Date,t=new Date(this.currentTokens.expires_at);return e.getTime()>=t.getTime()-6e4&&await this.refreshToken(),this.currentTokens.access_token}async refreshToken(){if(this.refreshPromise)return this.refreshPromise;if(!this.currentTokens)throw new s("No tokens available to refresh","NO_REFRESH_TOKEN");this.refreshPromise=this.performTokenRefresh();try{const e=await this.refreshPromise;return this.currentTokens=e,this.storage&&await this.storage.save(e),this.refreshCallback&&this.refreshCallback(e.access_token,e.refresh_token,e.expires_at),e}finally{this.refreshPromise=void 0}}async performTokenRefresh(){if(!this.currentTokens)throw new s("No tokens available","NO_TOKENS");const e=new URLSearchParams({grant_type:"refresh_token",client_id:this.keystring,refresh_token:this.currentTokens.refresh_token});try{const t=await this.fetch("https://api.etsy.com/v3/public/oauth/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:e.toString()});if(!t.ok){const e=await t.text();throw new s(`Token refresh failed: ${t.status} ${t.statusText} - ${e}`,"TOKEN_REFRESH_FAILED")}const r=await t.json();return{access_token:r.access_token,refresh_token:r.refresh_token,expires_at:new Date(Date.now()+1e3*r.expires_in),token_type:r.token_type,scope:r.scope}}catch(e){if(e instanceof s)throw e;throw new s(`Token refresh failed: ${e instanceof Error?e.message:"Unknown error"}`,"TOKEN_REFRESH_ERROR")}}getCurrentTokens(){return this.currentTokens?{...this.currentTokens}:null}updateTokens(e){this.currentTokens={...e}}isTokenExpired(){if(!this.currentTokens)return!0;const e=new Date,t=new Date(this.currentTokens.expires_at);return e.getTime()>=t.getTime()}willTokenExpireSoon(e=5){if(!this.currentTokens)return!0;const t=new Date,s=new Date(this.currentTokens.expires_at),r=60*e*1e3;return t.getTime()>=s.getTime()-r}async clearTokens(){this.currentTokens=null,this.storage&&await this.storage.clear()}getTimeUntilExpiration(){if(!this.currentTokens)return null;const e=new Date;return new Date(this.currentTokens.expires_at).getTime()-e.getTime()}async fetch(e,t){return l(),fetch(e,t)}}class f{constructor(e="etsy_tokens"){if(!h)throw new Error("localStorage is not available in this environment");this.storageKey=e}async save(e){const t=JSON.stringify(e);localStorage.setItem(this.storageKey,t)}async load(){try{const e=localStorage.getItem(this.storageKey);if(!e)return null;const t=JSON.parse(e);return t.expires_at&&(t.expires_at=new Date(t.expires_at)),t}catch{return null}}async clear(){localStorage.removeItem(this.storageKey)}}class y{constructor(e="etsy_tokens"){if(!u)throw new Error("sessionStorage is not available in this environment");this.storageKey=e}async save(e){const t=JSON.stringify(e);sessionStorage.setItem(this.storageKey,t)}async load(){try{const e=sessionStorage.getItem(this.storageKey);if(!e)return null;const t=JSON.parse(e);return t.expires_at&&(t.expires_at=new Date(t.expires_at)),t}catch{return null}}async clear(){sessionStorage.removeItem(this.storageKey)}}class w{constructor(e){this.requestCount=0,this.dailyReset=new Date,this.lastRequestTime=0,this.config={maxRequestsPerDay:1e4,maxRequestsPerSecond:10,minRequestInterval:100,...e},this.setNextDailyReset()}setNextDailyReset(){const e=new Date;this.dailyReset=new Date(e),this.dailyReset.setUTCDate(this.dailyReset.getUTCDate()+1),this.dailyReset.setUTCHours(0,0,0,0)}async waitForRateLimit(){const e=Date.now();if(e>=this.dailyReset.getTime()&&(this.requestCount=0,this.setNextDailyReset()),this.requestCount>=this.config.maxRequestsPerDay){const t=this.dailyReset.getTime()-e;throw new r(`Daily rate limit of ${this.config.maxRequestsPerDay} requests exceeded. Reset in ${Math.ceil(t/1e3/60)} minutes.`,t)}const t=e-this.lastRequestTime;if(t<this.config.minRequestInterval){const e=this.config.minRequestInterval-t;await new Promise(t=>setTimeout(t,e))}this.requestCount++,this.lastRequestTime=Date.now()}getRateLimitStatus(){const e=Date.now();return e>=this.dailyReset.getTime()&&(this.requestCount=0,this.setNextDailyReset()),{remainingRequests:Math.max(0,this.config.maxRequestsPerDay-this.requestCount),resetTime:this.dailyReset,canMakeRequest:this.requestCount<this.config.maxRequestsPerDay&&e-this.lastRequestTime>=this.config.minRequestInterval}}getRemainingRequests(){return Math.max(0,this.config.maxRequestsPerDay-this.requestCount)}reset(){this.requestCount=0,this.lastRequestTime=0,this.setNextDailyReset()}canMakeRequest(){const e=Date.now();return e>=this.dailyReset.getTime()&&(this.requestCount=0,this.setNextDailyReset()),!(this.requestCount>=this.config.maxRequestsPerDay)&&e-this.lastRequestTime>=this.config.minRequestInterval}getTimeUntilNextRequest(){const e=Date.now()-this.lastRequestTime;return e>=this.config.minRequestInterval?0:this.config.minRequestInterval-e}getConfig(){return{...this.config}}}const _=new w;class m{debug(e,...t){("localhost"===window.location?.hostname||"127.0.0.1"===window.location?.hostname)&&console.log(`[DEBUG] ${e}`,...t)}info(e,...t){console.log(`[INFO] ${e}`,...t)}warn(e,...t){console.warn(`[WARN] ${e}`,...t)}error(e,...t){console.error(`[ERROR] ${e}`,...t)}}class p{constructor(){this.cache=new Map}async get(e){const t=this.cache.get(e);return t?Date.now()>t.expires?(this.cache.delete(e),null):t.value:null}async set(e,t,s=3600){const r=Date.now()+1e3*s;this.cache.set(e,{value:t,expires:r})}async delete(e){this.cache.delete(e)}async clear(){this.cache.clear()}}class R{constructor(e){this.tokenManager=new d(e),this.baseUrl=e.baseUrl||"https://api.etsy.com/v3/application",this.logger=new m,this.keystring=e.keystring,!1!==e.rateLimiting?.enabled?this.rateLimiter=new w({maxRequestsPerDay:e.rateLimiting?.maxRequestsPerDay||1e4,maxRequestsPerSecond:e.rateLimiting?.maxRequestsPerSecond||10,minRequestInterval:e.rateLimiting?.minRequestInterval??100}):this.rateLimiter=new w(void 0),!1!==e.caching?.enabled&&(this.cache=e.caching?.storage||new p,this.cacheTtl=e.caching?.ttl||3600)}async makeRequest(e,r={},i=!0){const n=`${this.baseUrl}${e}`,o={method:"GET",...r},a=`${n}:${JSON.stringify(o)}`;if(i&&this.cache&&"GET"===o.method){const e=await this.cache.get(a);if(e)return JSON.parse(e)}await this.rateLimiter.waitForRateLimit();const c={Authorization:`Bearer ${await this.tokenManager.getAccessToken()}`,"x-api-key":this.getApiKey(),"Content-Type":"application/json",Accept:"application/json",...o.headers};try{const e=await this.fetch(n,{...o,headers:c});if(!e.ok){const s=await e.text();throw new t(`Etsy API error: ${e.status} ${e.statusText}`,e.status,s)}const s=await e.json();return i&&this.cache&&"GET"===o.method&&await this.cache.set(a,JSON.stringify(s),this.cacheTtl),s}catch(e){if(e instanceof t||e instanceof s)throw e;throw new t(`Request failed: ${e instanceof Error?e.message:"Unknown error"}`,0,e)}}getApiKey(){return this.keystring}async getUser(){return this.makeRequest("/users/me")}async getShop(e){if(e)return this.makeRequest(`/shops/${e}`);const s=await this.getUser();if(!s.shop_id)throw new t("User does not have a shop",404);return this.makeRequest(`/shops/${s.shop_id}`)}async getShopByOwnerUserId(e){return this.makeRequest(`/users/${e}/shops`)}async getShopSections(e){let s=e;if(!s){const e=await this.getUser();if(!e.shop_id)throw new t("User does not have a shop",404);s=e.shop_id.toString()}const r=await this.makeRequest(`/shops/${s}/sections`);return this.logger.info(`Found ${r.results.length} shop sections`),r.results}async getShopSection(e,t){return this.makeRequest(`/shops/${e}/sections/${t}`)}async getListingsByShop(e,s={}){let r=e;if(!r){const e=await this.getUser();if(!e.shop_id)throw new t("User does not have a shop",404);r=e.shop_id.toString()}const i=new URLSearchParams;i.set("state",s.state||"active"),void 0!==s.limit&&i.set("limit",s.limit.toString()),void 0!==s.offset&&i.set("offset",s.offset.toString()),s.sort_on&&i.set("sort_on",s.sort_on),s.sort_order&&i.set("sort_order",s.sort_order),s.includes&&i.set("includes",s.includes.join(","));return(await this.makeRequest(`/shops/${r}/listings?${i.toString()}`)).results}async getListing(e,t){const s=t?`?includes=${t.join(",")}`:"";return this.makeRequest(`/listings/${e}${s}`)}async findAllListingsActive(e={}){const t=new URLSearchParams;e.keywords&&t.set("keywords",e.keywords),e.category&&t.set("category",e.category),void 0!==e.limit&&t.set("limit",e.limit.toString()),void 0!==e.offset&&t.set("offset",e.offset.toString()),e.sort_on&&t.set("sort_on",e.sort_on),e.sort_order&&t.set("sort_order",e.sort_order),void 0!==e.min_price&&t.set("min_price",e.min_price.toString()),void 0!==e.max_price&&t.set("max_price",e.max_price.toString()),e.tags&&t.set("tags",e.tags.join(",")),e.location&&t.set("location",e.location),e.shop_location&&t.set("shop_location",e.shop_location);return(await this.makeRequest(`/listings/active?${t.toString()}`)).results}async getListingImages(e){return(await this.makeRequest(`/listings/${e}/images`)).results}async getListingInventory(e){return this.makeRequest(`/listings/${e}/inventory`)}async getSellerTaxonomyNodes(){return(await this.makeRequest("/seller-taxonomy/nodes")).results}async getUserShops(){return(await this.makeRequest("/users/me/shops")).results||[]}getRemainingRequests(){return this.rateLimiter.getRemainingRequests()}getRateLimitStatus(){return this.rateLimiter.getRateLimitStatus()}async clearCache(){this.cache&&await this.cache.clear()}getCurrentTokens(){return this.tokenManager.getCurrentTokens()}isTokenExpired(){return this.tokenManager.isTokenExpired()}async refreshToken(){return this.tokenManager.refreshToken()}async fetch(e,t){return l(),fetch(e,t)}}const E="undefined"!=typeof window;async function T(e){const t=await async function(e){if(E){const t=new Uint8Array(e);return crypto.getRandomValues(t),t}throw new Error("Crypto functions are not available in this environment")}(e);return A(t)}async function S(e){const t=new TextEncoder,s="string"==typeof e?t.encode(e):e;if(E){const e=await crypto.subtle.digest("SHA-256",s);return new Uint8Array(e)}throw new Error("SHA256 is not available in this environment")}async function k(e){return A(await S(e))}function A(e){if(E){const t=String.fromCharCode(...e);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}throw new Error("Base64URL encoding is not available in this environment")}async function D(){return T(32)}async function I(){return T(32)}async function v(e){return k(e)}class x{constructor(e){this.keystring=e.keystring,this.redirectUri=e.redirectUri,this.scopes=e.scopes,this.codeVerifier=e.codeVerifier||"",this.state=e.state||"",this.initialized=this.initialize(e)}async initialize(e){e.codeVerifier||(this.codeVerifier=await D()),e.state||(this.state=await I())}async getAuthUrl(){await this.initialized;const e=await v(this.codeVerifier);return`https://www.etsy.com/oauth/connect?${new URLSearchParams({response_type:"code",client_id:this.keystring,redirect_uri:this.redirectUri,scope:this.scopes.join(" "),state:this.state,code_challenge:e,code_challenge_method:"S256"}).toString()}`}async setAuthorizationCode(e,t){if(await this.initialized,t!==this.state)throw new s("State parameter mismatch","INVALID_STATE");this.authorizationCode=e,this.receivedState=t}async getAccessToken(){if(!this.authorizationCode)throw new s("Authorization code not set. Call setAuthorizationCode() first.","NO_AUTH_CODE");const e=new URLSearchParams({grant_type:"authorization_code",client_id:this.keystring,redirect_uri:this.redirectUri,code:this.authorizationCode,code_verifier:this.codeVerifier});try{l();const t=await fetch("https://api.etsy.com/v3/public/oauth/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:e.toString()});if(!t.ok){const e=await t.text();throw new s(`Token exchange failed: ${t.status} ${t.statusText} - ${e}`,"TOKEN_EXCHANGE_FAILED")}const r=await t.json();return{access_token:r.access_token,refresh_token:r.refresh_token,expires_at:new Date(Date.now()+1e3*r.expires_in),token_type:r.token_type,scope:r.scope}}catch(e){if(e instanceof s)throw e;throw new s(`Token exchange failed: ${e instanceof Error?e.message:"Unknown error"}`,"TOKEN_EXCHANGE_ERROR")}}async getState(){return await this.initialized,this.state}async getCodeVerifier(){return await this.initialized,this.codeVerifier}getScopes(){return[...this.scopes]}getRedirectUri(){return this.redirectUri}}const N={LISTINGS_READ:"listings_r",SHOPS_READ:"shops_r",PROFILE_READ:"profile_r",FAVORITES_READ:"favorites_r",FEEDBACK_READ:"feedback_r",TREASURY_READ:"treasury_r",LISTINGS_WRITE:"listings_w",SHOPS_WRITE:"shops_w",PROFILE_WRITE:"profile_w",FAVORITES_WRITE:"favorites_w",FEEDBACK_WRITE:"feedback_w",TREASURY_WRITE:"treasury_w",LISTINGS_DELETE:"listings_d",SHOPS_DELETE:"shops_d",PROFILE_DELETE:"profile_d",FAVORITES_DELETE:"favorites_d",FEEDBACK_DELETE:"feedback_d",TREASURY_DELETE:"treasury_d",TRANSACTIONS_READ:"transactions_r",TRANSACTIONS_WRITE:"transactions_w",BILLING_READ:"billing_r",CART_READ:"cart_r",CART_WRITE:"cart_w",RECOMMEND_READ:"recommend_r",RECOMMEND_WRITE:"recommend_w",ADDRESS_READ:"address_r",ADDRESS_WRITE:"address_w",EMAIL_READ:"email_r"},C={SHOP_READ_ONLY:[N.SHOPS_READ,N.LISTINGS_READ,N.PROFILE_READ],SHOP_MANAGEMENT:[N.SHOPS_READ,N.SHOPS_WRITE,N.LISTINGS_READ,N.LISTINGS_WRITE,N.LISTINGS_DELETE,N.PROFILE_READ,N.TRANSACTIONS_READ],BASIC_ACCESS:[N.SHOPS_READ,N.LISTINGS_READ]};const q="1.0.2",L="etsy-v3-api-client";e.AuthHelper=x,e.COMMON_SCOPE_COMBINATIONS=C,e.ETSY_SCOPES=N,e.EtsyApiError=t,e.EtsyAuthError=s,e.EtsyClient=R,e.EtsyRateLimitError=r,e.EtsyRateLimiter=w,e.FileTokenStorage=class{constructor(e){throw new Error("FileTokenStorage is only available in Node.js environments")}async save(e){throw new Error("FileTokenStorage is only available in Node.js")}async load(){return null}async clear(){}async _writeFile(e,t){}async _readFile(e){if("undefined"==typeof process)throw new Error("Not available");throw new Error("readFile not available in browser")}async _deleteFile(e){}},e.LIBRARY_NAME=L,e.LocalStorageTokenStorage=f,e.MemoryTokenStorage=g,e.SessionStorageTokenStorage=y,e.TokenManager=d,e.VERSION=q,e.createAuthHelper=function(e){return new x(e)},e.createCodeChallenge=v,e.createDefaultTokenStorage=function(e){return e?.preferSession&&u?new y(e?.storageKey):h?new f(e?.storageKey):u?new y(e?.storageKey):new g},e.createEtsyClient=function(e){return new R(e)},e.createRateLimiter=function(e){return new w(e)},e.createTokenManager=function(e,t){return new d(e,t)},e.default=R,e.defaultRateLimiter=_,e.generateCodeVerifier=D,e.generateRandomBase64Url=T,e.generateState=I,e.getAvailableStorage=function(){return h?"localStorage":u?"sessionStorage":"memory"},e.getEnvironmentInfo=function(){return{isBrowser:i,isNode:Boolean(n),isWebWorker:o,hasFetch:a,hasWebCrypto:c,hasLocalStorage:h,hasSessionStorage:u,userAgent:i?navigator.userAgent:"Node.js",nodeVersion:void 0}},e.getLibraryInfo=function(){return{name:L,version:q,description:"JavaScript/TypeScript client for the Etsy Open API v3 with OAuth 2.0 authentication",author:"profplum700",license:"MIT",homepage:"https://github.com/ForestHillArtsHouse/etsy-v3-api-client#readme"}},e.hasLocalStorage=h,e.hasSessionStorage=u,e.isBrowser=i,e.isNode=n,e.sha256=S,e.sha256Base64Url=k,Object.defineProperty(e,"__esModule",{value:!0})});
//# sourceMappingURL=browser.umd.js.map