ebay-mcp 1.18.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -16
- package/build/api/account-management/account.js +2 -2
- package/build/api/account-management/accountV2.js +0 -6
- package/build/api/account-management/finances.js +7 -5
- package/build/api/client/ebayApiClient.js +111 -10
- package/build/api/client/signEbayRequest.js +124 -0
- package/build/api/communication/feedback.js +4 -4
- package/build/api/developer/developer.js +43 -7
- package/build/api/ebaySellerApi.js +0 -4
- package/build/api/marketing-and-promotions/campaigns.js +0 -34
- package/build/api/order-management/dispute.js +29 -20
- package/build/api/order-management/fulfillment.js +2 -1
- package/build/api/other/edelivery.js +3 -3
- package/build/api/other/translation.js +1 -1
- package/build/config/environment.js +31 -3
- package/build/schemas/account-management/account.js +1 -1
- package/build/schemas/account-management/accountV2.js +3 -4
- package/build/schemas/communication/messages.js +2 -2
- package/build/schemas/marketing/campaigns.js +0 -21
- package/build/schemas/other/otherApis.js +2 -85
- package/build/scripts/devSync.js +75 -225
- package/build/scripts/specCoverage.js +78 -0
- package/build/scripts/specDownload.js +42 -0
- package/build/scripts/specRefs.js +2 -3
- package/build/scripts/specSources.js +4 -6
- package/build/scripts/specTypeNames.js +23 -0
- package/build/skills/content.js +2 -2
- package/build/tools/categories/account.js +3 -3
- package/build/tools/categories/communication.js +5 -5
- package/build/tools/categories/developer.js +1 -1
- package/build/tools/categories/finances.js +2 -2
- package/build/tools/categories/marketing.js +0 -16
- package/build/tools/categories/other.js +3 -17
- package/build/types/ebay.js +0 -1
- package/build/types/ebayEnums.js +0 -40
- package/build/ui/card.html +23 -23
- package/build/ui/chart.html +22 -22
- package/build/ui/stat.html +23 -23
- package/build/ui/table.html +21 -21
- package/build/utils/communication/feedback.js +3 -3
- package/build/utils/errors.js +1 -0
- package/package.json +5 -5
- package/build/api/other/compliance.js +0 -60
- package/build/utils/other/compliance.js +0 -56
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>The eBay MCP server — give Claude, Cursor, and any AI assistant full access to eBay's Sell APIs.
|
|
8
|
+
<strong>The eBay MCP server — give Claude, Cursor, and any AI assistant full access to eBay's Sell APIs. 380 tools for inventory, orders, marketing, and analytics, running locally with your own keys.</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center"><sub>Unofficial, open-source project — not affiliated with, authorized, or endorsed by eBay Inc.</sub></p>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
23
|
-
<img src="https://img.shields.io/badge/tools-
|
|
23
|
+
<img src="https://img.shields.io/badge/tools-380-8957e5?logo=ebay&logoColor=white" alt="380 eBay API tools" />
|
|
24
24
|
<img src="https://img.shields.io/badge/Sell%20API%20coverage-100%25-success" alt="100% eBay Sell API coverage" />
|
|
25
25
|
<img src="https://img.shields.io/badge/Model%20Context%20Protocol-compatible-000000" alt="Model Context Protocol compatible" />
|
|
26
26
|
<img src="https://img.shields.io/badge/tests-1%2C000%2B%20passing-3fb950?logo=vitest&logoColor=white" alt="Over 1,000 passing tests" />
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
-
**eBay MCP** is a local [Model Context Protocol](https://modelcontextprotocol.io) server that connects AI assistants — [Claude Desktop](https://claude.ai/download), [Claude Code](https://code.claude.com/docs/en/overview), [Cursor](https://cursor.com/), [Cline](https://cline.bot/), [Windsurf](https://windsurf.com/), [Zed](https://zed.dev/), [Continue.dev](https://docs.continue.dev/), [Roo Code](https://roocode.com/), and [Amazon Q Developer](https://aws.amazon.com/q/developer/) — directly to **[eBay's Sell APIs](https://developer.ebay.com/api-docs/sell/static/overview.html)**. It exposes **
|
|
36
|
+
**eBay MCP** is a local [Model Context Protocol](https://modelcontextprotocol.io) server that connects AI assistants — [Claude Desktop](https://claude.ai/download), [Claude Code](https://code.claude.com/docs/en/overview), [Cursor](https://cursor.com/), [Cline](https://cline.bot/), [Windsurf](https://windsurf.com/), [Zed](https://zed.dev/), [Continue.dev](https://docs.continue.dev/), [Roo Code](https://roocode.com/), and [Amazon Q Developer](https://aws.amazon.com/q/developer/) — directly to **[eBay's Sell APIs](https://developer.ebay.com/api-docs/sell/static/overview.html)**. It exposes **380 tools** spanning **100% of the eBay Sell, Commerce and Developer APIs it tracks** (356 unique endpoints) for inventory management, order fulfillment, promoted-listings marketing, analytics, and developer tooling. Everything runs on your machine over STDIO or local HTTP — **no cloud relay**, and your eBay credentials never leave your computer.
|
|
37
37
|
|
|
38
38
|
> **Disclaimer:** Unofficial, third-party project — **not affiliated with or endorsed by eBay Inc.** Provided "as is" without warranty. You are responsible for complying with [eBay's API License Agreement](https://developer.ebay.com/join/api-license-agreement) and [data-handling requirements](https://developer.ebay.com/api-docs/static/data-handling-update.html), keeping your credentials secure, and staying within rate limits. Test in sandbox before production. See [LICENSE](LICENSE).
|
|
39
39
|
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
## Features
|
|
61
61
|
|
|
62
|
-
- **
|
|
62
|
+
- **380 eBay API tools** — 100% coverage of the eBay Sell APIs across inventory, orders, marketing, analytics, metadata, taxonomy, and developer tooling.
|
|
63
63
|
- **9 AI clients, auto-configured** — Claude Desktop, Cursor, Zed, Cline, Continue.dev, Windsurf, Roo Code, Claude Code CLI, and Amazon Q Developer.
|
|
64
64
|
- **OAuth 2.0 built in** — full user-token management with automatic refresh, and smart fallback from user tokens (10k–50k req/day) to client credentials (1k req/day).
|
|
65
65
|
- **Resilient by default** — automatic retry with exponential backoff on `429` rate limits, and consistent, loud error surfacing.
|
|
@@ -87,7 +87,7 @@ Use this map when deciding which tool family to expose, or when asking an assist
|
|
|
87
87
|
| `analytics` | Traffic reports, seller standards, and customer-service metrics | "Show my seller standards profile." |
|
|
88
88
|
| `communication` | Buyer-seller messaging, negotiations, notifications, and feedback | "Show recent buyer messages that need a response." |
|
|
89
89
|
| `metadata` / `taxonomy` | Category trees, aspects, item conditions, return-policy metadata, tax jurisdictions, vehicle compatibility, shipping carriers/services/locations, handling times, expired categories, bulk aspect export, and charitable organizations | "Find required item aspects for this category." |
|
|
90
|
-
| `other` | Identity, VeRO, translation, and international shipping support APIs
|
|
90
|
+
| `other` | Identity, VeRO, translation, and international shipping support APIs | "Show my current seller identity details." |
|
|
91
91
|
| `developer` / `token-management` | Rate limits, signing keys, OAuth URLs, token refresh, and diagnostics | "Check my eBay API rate limits." |
|
|
92
92
|
| `trading` | Legacy XML listing create, revise, relist, and end operations for fixed-price listings and auctions | "Create a fixed-price listing draft from this SKU." |
|
|
93
93
|
| `connector` | ChatGPT connector search/fetch tools over the eBay MCP catalogue | "Search the eBay tool catalogue for order tools." |
|
|
@@ -251,9 +251,23 @@ billing agreement; `ebay_cancel_shipment` refunds it while the label is unused.
|
|
|
251
251
|
release.
|
|
252
252
|
|
|
253
253
|
Eligible keysets add the scope to `EBAY_OAUTH_SCOPES`, keeping the other scopes they need, and
|
|
254
|
-
repeat OAuth consent.
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
repeat OAuth consent.
|
|
255
|
+
|
|
256
|
+
**Digital signatures (EU/UK sellers).** eBay requires
|
|
257
|
+
[Digital Signatures](https://developer.ebay.com/develop/guides/digital-signatures-for-apis) on every
|
|
258
|
+
Finances API call and on `issueRefund` for sellers domiciled in the EU or UK; without them eBay
|
|
259
|
+
answers `403 Missing x-ebay-signature-key header`. To enable signing:
|
|
260
|
+
|
|
261
|
+
1. Run `ebay_create_signing_key` (ED25519 is recommended) and keep the response — eBay never shows
|
|
262
|
+
the private key again.
|
|
263
|
+
2. Set `EBAY_SIGNING_KEY_JWE` to its `jwe` and `EBAY_SIGNING_PRIVATE_KEY` to its `privateKey`
|
|
264
|
+
(the base64 value as returned, or a PEM key), then restart the server.
|
|
265
|
+
|
|
266
|
+
Those calls then carry the `x-ebay-signature-key`, `Content-Digest`, `Signature-Input`, and
|
|
267
|
+
`Signature` headers (RFC 9421). Other calls are never signed, and nothing changes while the two
|
|
268
|
+
variables are unset. Behind `EBAY_MCP_API_BASE_URL`, calls are signed for the eBay host and path
|
|
269
|
+
they reach (`api.ebay.com` or `apiz.ebay.com`), so the proxy must forward eBay's `Host` header and
|
|
270
|
+
the original eBay path.
|
|
257
271
|
|
|
258
272
|
## eBay MCP vs. the raw eBay API
|
|
259
273
|
|
|
@@ -267,7 +281,7 @@ Both talk to the same eBay endpoints — the difference is everything you'd othe
|
|
|
267
281
|
| Input validation | Zod schemas + TypeScript types on every tool | None — you validate your own payloads |
|
|
268
282
|
| Setup | One wizard (`npm run setup`) | Per-call auth, headers, and marketplace wiring |
|
|
269
283
|
| AI client support | 9 clients auto-configured | Not applicable |
|
|
270
|
-
| API coverage |
|
|
284
|
+
| API coverage | 380 tools across 100% of the Sell APIs, ready to call | Build each request from the docs |
|
|
271
285
|
| Hosting | Runs locally, no cloud relay | Your own infrastructure |
|
|
272
286
|
|
|
273
287
|
## One-click AI setup
|
|
@@ -392,6 +406,8 @@ EBAY_MARKETPLACE_ID=EBAY_US # default marketplace (overridable per tool)
|
|
|
392
406
|
# EBAY_SITE_ID=0 # Trading API site override; defaults from EBAY_MARKETPLACE_ID
|
|
393
407
|
EBAY_CONTENT_LANGUAGE=en-US # default request content language
|
|
394
408
|
EBAY_USER_REFRESH_TOKEN=your_token # for higher rate limits
|
|
409
|
+
# EBAY_SIGNING_KEY_JWE=<jwe> # Digital Signatures for EU/UK sellers (jwe from ebay_create_signing_key)
|
|
410
|
+
# EBAY_SIGNING_PRIVATE_KEY=<key> # privateKey from the same response (base64 PKCS#8 or PEM)
|
|
395
411
|
EBAY_MCP_UI=on # interactive MCP Apps views (beta); "off" forces plain JSON
|
|
396
412
|
EBAY_MCP_TOOLS=all # tool exposure: "all", "dynamic", or a family list (see below)
|
|
397
413
|
EBAY_READ_ONLY=false # when true, only register read-only tools (gets/lists/searches)
|
|
@@ -428,7 +444,7 @@ User-token limits vary by account tier (Individual 10k · Commercial 25k · Ente
|
|
|
428
444
|
|
|
429
445
|
### MCP client compatibility
|
|
430
446
|
|
|
431
|
-
Auto-configured by `npm run setup`. Requires [Node.js](https://nodejs.org/en) ≥
|
|
447
|
+
Auto-configured by `npm run setup`. Requires [Node.js](https://nodejs.org/en) ≥ 24 and MCP protocol 1.0+ over STDIO (default) or HTTP.
|
|
432
448
|
|
|
433
449
|
| Client | Platform | Config path |
|
|
434
450
|
| ---------------------- | --------------------- | ---------------------------------------------------------------------------- |
|
|
@@ -447,9 +463,9 @@ Auto-configured by `npm run setup`. Requires [Node.js](https://nodejs.org/en)
|
|
|
447
463
|
## Available tools
|
|
448
464
|
|
|
449
465
|
<details open>
|
|
450
|
-
<summary><strong>
|
|
466
|
+
<summary><strong>380 tools by category (100% Sell API coverage)</strong></summary>
|
|
451
467
|
|
|
452
|
-
**
|
|
468
|
+
**380 tools**, 100% Sell API coverage, organized by category. Each link points to the tool definitions and handlers in [`src/tools/categories/`](src/tools/categories/):
|
|
453
469
|
|
|
454
470
|
| Category | What you can do |
|
|
455
471
|
| --- | --- |
|
|
@@ -466,7 +482,7 @@ Auto-configured by `npm run setup`. Requires [Node.js](https://nodejs.org/en)
|
|
|
466
482
|
| [Communication](src/tools/categories/communication.ts) | Buyer–seller messaging, negotiations, notifications, feedback |
|
|
467
483
|
| [Metadata](src/tools/categories/metadata.ts) | Return policies, sales-tax jurisdictions, automotive compatibility, shipping carriers/services/locations, handling times |
|
|
468
484
|
| [Taxonomy](src/tools/categories/taxonomy.ts) | Category trees, item aspects, item conditions, expired categories, bulk aspect export, and charitable organizations ([`charity.ts`](src/tools/categories/charity.ts), Charity API) |
|
|
469
|
-
| [Other](src/tools/categories/other.ts) | Identity, VeRO, translation, and international shipping support APIs
|
|
485
|
+
| [Other](src/tools/categories/other.ts) | Identity, VeRO, translation, and international shipping support APIs |
|
|
470
486
|
| [Trading (legacy XML)](src/tools/categories/trading.ts) | Fixed-price and auction listing create, revise, relist, end |
|
|
471
487
|
| [Developer](src/tools/categories/developer.ts) | Rate limits, signing keys, client registration |
|
|
472
488
|
| [Token Management](src/tools/categories/tokenManagement.ts) | OAuth URL generation and token management |
|
|
@@ -537,7 +553,7 @@ Common tasks, phrased as you'd ask your AI assistant:
|
|
|
537
553
|
<details>
|
|
538
554
|
<summary><strong>What is the eBay MCP server?</strong></summary>
|
|
539
555
|
|
|
540
|
-
A local [Model Context Protocol](https://modelcontextprotocol.io) server that exposes **
|
|
556
|
+
A local [Model Context Protocol](https://modelcontextprotocol.io) server that exposes **380 tools** covering **100% of the eBay Sell, Commerce and Developer APIs it tracks** (356 endpoints) to AI assistants — inventory, order fulfillment, marketing, analytics, and developer tools.
|
|
541
557
|
|
|
542
558
|
</details>
|
|
543
559
|
|
|
@@ -572,7 +588,7 @@ Interactive [MCP Apps](#interactive-ui-mcp-apps) views only appear on hosts that
|
|
|
572
588
|
<details>
|
|
573
589
|
<summary><strong>How many eBay APIs and tools does it cover?</strong></summary>
|
|
574
590
|
|
|
575
|
-
|
|
591
|
+
380 tools across 356 unique endpoints — 100% of the eBay Sell, Commerce and Developer APIs it tracks.
|
|
576
592
|
|
|
577
593
|
</details>
|
|
578
594
|
|
|
@@ -593,7 +609,7 @@ It runs entirely on your machine over STDIO (or local HTTP). There is no cloud r
|
|
|
593
609
|
<details>
|
|
594
610
|
<summary><strong>What do I need to get started?</strong></summary>
|
|
595
611
|
|
|
596
|
-
Node.js ≥
|
|
612
|
+
Node.js ≥ 24, a free [eBay Developer Account](https://developer.ebay.com/) (Client ID + Client Secret), then run `npm run setup`.
|
|
597
613
|
|
|
598
614
|
</details>
|
|
599
615
|
|
|
@@ -685,6 +701,8 @@ pnpm test && pnpm test:integration # Vitest unit + integration
|
|
|
685
701
|
pnpm build # Build to build/
|
|
686
702
|
```
|
|
687
703
|
|
|
704
|
+
To run the CI gate locally in Docker before pushing, use [act](https://nektosact.com): `act workflow_dispatch -W .github/workflows/ci.yml`. The root `.actrc` picks the runner image and keeps the pnpm store outside the workspace.
|
|
705
|
+
|
|
688
706
|
## Resources
|
|
689
707
|
|
|
690
708
|
Project docs:
|
|
@@ -608,12 +608,12 @@ export class AccountApi {
|
|
|
608
608
|
*
|
|
609
609
|
* @example
|
|
610
610
|
* ```ts
|
|
611
|
-
* const kyc = await Effect.runPromise(accountApi.
|
|
611
|
+
* const kyc = await Effect.runPromise(accountApi.getKYC({}));
|
|
612
612
|
* ```
|
|
613
613
|
*
|
|
614
614
|
* @see https://developer.ebay.com/api-docs/sell/account/resources/kyc/methods/getKYC
|
|
615
615
|
*/
|
|
616
|
-
|
|
616
|
+
getKYC = (_input = {}) => requestGetEffect(this.client, `${ACCOUNT_BASE_PATH}/kyc`);
|
|
617
617
|
/**
|
|
618
618
|
* Retrieves seller advertising-program eligibility for one marketplace.
|
|
619
619
|
*
|
|
@@ -299,12 +299,6 @@ export class AccountV2Api {
|
|
|
299
299
|
* Changes one or more seller preferences for one marketplace (PATCH: send only
|
|
300
300
|
* the preferences being changed).
|
|
301
301
|
*
|
|
302
|
-
* eBay's OAS3 spec references an undefined `SetUserPreferencesRequest`, so the
|
|
303
|
-
* generated body type is `unknown`. The body is typed from the tool schema,
|
|
304
|
-
* which mirrors the definition in eBay's published Account v2 spec: the
|
|
305
|
-
* `*PreferencesType` containers returned by getUserPreferences, with
|
|
306
|
-
* SetItemsAwaitingPaymentPreferencesType for unpaid-item settings.
|
|
307
|
-
*
|
|
308
302
|
* @param input - Marketplace header and the preferences PATCH body.
|
|
309
303
|
* @returns An Effect that succeeds when eBay returns 204 No Content.
|
|
310
304
|
*
|
|
@@ -14,7 +14,9 @@ const pageQuery = (input) => buildEndpointParams({
|
|
|
14
14
|
const filterQuery = (filter) => buildEndpointParams({ filter: { wireName: 'filter', value: filter } });
|
|
15
15
|
/**
|
|
16
16
|
* Finances API - payouts, transactions, transfers, seller funds, billing activity, and order
|
|
17
|
-
* earnings. Every operation except getBillingActivities is served from the apiz host.
|
|
17
|
+
* earnings. Every operation except getBillingActivities is served from the apiz host. Every
|
|
18
|
+
* call is digitally signed when signing credentials are configured, as eBay requires for
|
|
19
|
+
* EU/UK sellers.
|
|
18
20
|
*/
|
|
19
21
|
export class FinancesApi {
|
|
20
22
|
client;
|
|
@@ -28,8 +30,8 @@ export class FinancesApi {
|
|
|
28
30
|
getFromApiz = (path, params, marketplaceId) => {
|
|
29
31
|
const client = this.client;
|
|
30
32
|
const config = marketplaceId === undefined
|
|
31
|
-
? { absolute: true }
|
|
32
|
-
: { absolute: true, headers: { 'X-EBAY-C-MARKETPLACE-ID': marketplaceId } };
|
|
33
|
+
? { absolute: true, signed: true }
|
|
34
|
+
: { absolute: true, signed: true, headers: { 'X-EBAY-C-MARKETPLACE-ID': marketplaceId } };
|
|
33
35
|
return Effect.tryPromise({
|
|
34
36
|
try: () => {
|
|
35
37
|
const { environment, apiBaseUrl } = client.getConfig();
|
|
@@ -281,8 +283,8 @@ export class FinancesApi {
|
|
|
281
283
|
return Effect.gen(function* () {
|
|
282
284
|
const validatedInput = yield* decodeEndpointInputEffect(getBillingActivitiesInputSchema, input);
|
|
283
285
|
const config = validatedInput.acceptLanguage === undefined
|
|
284
|
-
?
|
|
285
|
-
: { headers: { 'Accept-Language': validatedInput.acceptLanguage } };
|
|
286
|
+
? { signed: true }
|
|
287
|
+
: { signed: true, headers: { 'Accept-Language': validatedInput.acceptLanguage } };
|
|
286
288
|
return yield* requestGetEffect(client, `${BASE_PATH}/billing_activity`, pageQuery(validatedInput), config);
|
|
287
289
|
});
|
|
288
290
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EbayOAuthClient } from '../../auth/oauth.js';
|
|
2
2
|
import { clientRequestError, } from '../../api/client/ebayClientRequestError.js';
|
|
3
3
|
import { RateLimitTracker } from '../../api/client/rateLimitTracker.js';
|
|
4
|
-
import {
|
|
4
|
+
import { parseSigningPrivateKey, signEbayRequest } from '../../api/client/signEbayRequest.js';
|
|
5
|
+
import { getBaseUrl, getIdentityBaseUrl } from '../../config/environment.js';
|
|
5
6
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
6
7
|
import { httpRequestEffect, isHttpError, } from '../../utils/http.js';
|
|
7
8
|
import { isRecord } from '../../utils/typeGuards.js';
|
|
@@ -22,6 +23,38 @@ const deleteHeader = (headers, name) => {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Serialize a body exactly as the HTTP adapter would so its Content-Digest
|
|
28
|
+
* matches the bytes on the wire. Multipart and Blob bodies never reach here:
|
|
29
|
+
* fetch streams them, so they cannot be digested up front.
|
|
30
|
+
*/
|
|
31
|
+
const serializeSignedBody = (requestBody) => {
|
|
32
|
+
if (requestBody === undefined || requestBody === null) {
|
|
33
|
+
return { wireBody: requestBody };
|
|
34
|
+
}
|
|
35
|
+
if (typeof requestBody === 'string' || requestBody instanceof Uint8Array) {
|
|
36
|
+
return { wireBody: requestBody, digestBytes: requestBody };
|
|
37
|
+
}
|
|
38
|
+
if (requestBody instanceof ArrayBuffer) {
|
|
39
|
+
return { wireBody: requestBody, digestBytes: new Uint8Array(requestBody) };
|
|
40
|
+
}
|
|
41
|
+
if (requestBody instanceof URLSearchParams) {
|
|
42
|
+
const encoded = requestBody.toString();
|
|
43
|
+
return { wireBody: requestBody, digestBytes: encoded };
|
|
44
|
+
}
|
|
45
|
+
const json = JSON.stringify(requestBody);
|
|
46
|
+
return { wireBody: json, digestBytes: json };
|
|
47
|
+
};
|
|
48
|
+
// A proxy forwards eBay's Host header and path, so the signature covers the eBay URL.
|
|
49
|
+
const ebayFacingUrl = (url, ebayBaseUrl, apiBaseUrl) => apiBaseUrl !== undefined && url.startsWith(apiBaseUrl)
|
|
50
|
+
? `${ebayBaseUrl}${url.slice(apiBaseUrl.length)}`
|
|
51
|
+
: url;
|
|
52
|
+
const SIGNATURE_ERROR_PATTERN = /signature/i;
|
|
53
|
+
const TRAILING_PERIOD_PATTERN = /\.?$/;
|
|
54
|
+
/** True when an eBay error body points at a missing or rejected digital signature. */
|
|
55
|
+
const isSignatureFailure = (detail) => detail !== undefined && SIGNATURE_ERROR_PATTERN.test(detail);
|
|
56
|
+
/** Remediation appended to signature failures when the server has no signing credentials. */
|
|
57
|
+
const SIGNING_SETUP_HINT = ' eBay requires Digital Signatures on this call for EU/UK sellers. Create a key with ebay_create_signing_key, then set EBAY_SIGNING_KEY_JWE (the jwe value) and EBAY_SIGNING_PRIVATE_KEY (the privateKey value) and restart the server.';
|
|
25
58
|
/**
|
|
26
59
|
* Remediation hint for a token failure, matched to the token the request needs.
|
|
27
60
|
*
|
|
@@ -46,6 +79,8 @@ export class EbayApiClient {
|
|
|
46
79
|
rateLimitTracker;
|
|
47
80
|
config;
|
|
48
81
|
timeoutMs = 30_000;
|
|
82
|
+
/** Parsed signing key, cached after the first signed request. */
|
|
83
|
+
signingKey;
|
|
49
84
|
/**
|
|
50
85
|
* Build default request headers based on configured marketplace and language.
|
|
51
86
|
*/
|
|
@@ -70,6 +105,47 @@ export class EbayApiClient {
|
|
|
70
105
|
this.baseUrl = getBaseUrl(config.environment, config.apiBaseUrl);
|
|
71
106
|
this.rateLimitTracker = new RateLimitTracker();
|
|
72
107
|
}
|
|
108
|
+
/** Parse (once) and return the configured signing key. */
|
|
109
|
+
getSigningKey(privateKey) {
|
|
110
|
+
if (this.signingKey === undefined) {
|
|
111
|
+
this.signingKey = parseSigningPrivateKey(privateKey);
|
|
112
|
+
}
|
|
113
|
+
return this.signingKey;
|
|
114
|
+
}
|
|
115
|
+
signRequest(method, url, options) {
|
|
116
|
+
const { signing, environment, apiBaseUrl } = this.config;
|
|
117
|
+
if (!options.signed || signing === undefined) {
|
|
118
|
+
return Effect.succeed({ wireBody: options.data, signatureHeaders: {} });
|
|
119
|
+
}
|
|
120
|
+
if (options.data instanceof FormData || options.data instanceof Blob) {
|
|
121
|
+
return Effect.fail(clientRequestError({
|
|
122
|
+
kind: 'signing',
|
|
123
|
+
method,
|
|
124
|
+
url,
|
|
125
|
+
message: 'Multipart and Blob request bodies cannot be digitally signed.',
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
const { wireBody, digestBytes } = serializeSignedBody(options.data);
|
|
129
|
+
// Absolute signed endpoints are the apiz-hosted Finances calls.
|
|
130
|
+
const ebayBaseUrl = options.absolute
|
|
131
|
+
? getIdentityBaseUrl(environment)
|
|
132
|
+
: getBaseUrl(environment);
|
|
133
|
+
const signedUrl = ebayFacingUrl(url, ebayBaseUrl, apiBaseUrl);
|
|
134
|
+
return Effect.try({
|
|
135
|
+
try: () => {
|
|
136
|
+
const signingKey = this.getSigningKey(signing.privateKey);
|
|
137
|
+
const signatureHeaders = signEbayRequest({ method, url: signedUrl, bodyBytes: digestBytes }, signing.keyJwe, signingKey);
|
|
138
|
+
return { wireBody, signatureHeaders };
|
|
139
|
+
},
|
|
140
|
+
catch: (cause) => clientRequestError({
|
|
141
|
+
kind: 'signing',
|
|
142
|
+
method,
|
|
143
|
+
url,
|
|
144
|
+
message: `Failed to sign request: ${getErrorMessage(cause)}`,
|
|
145
|
+
cause,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
73
149
|
/**
|
|
74
150
|
* Acquire the access token a request should authenticate with.
|
|
75
151
|
*
|
|
@@ -207,6 +283,8 @@ export class EbayApiClient {
|
|
|
207
283
|
}
|
|
208
284
|
headers.Authorization = `Bearer ${token}`;
|
|
209
285
|
}
|
|
286
|
+
const { wireBody, signatureHeaders } = yield* this.signRequest(method, url, options);
|
|
287
|
+
Object.assign(headers, signatureHeaders);
|
|
210
288
|
this.rateLimitTracker.recordRequest();
|
|
211
289
|
logRequest(method, url, options.params, isBinaryBody(options.data) ? '[binary body]' : options.data);
|
|
212
290
|
return yield* httpRequestEffect({
|
|
@@ -214,7 +292,7 @@ export class EbayApiClient {
|
|
|
214
292
|
url,
|
|
215
293
|
params: options.params,
|
|
216
294
|
headers,
|
|
217
|
-
body:
|
|
295
|
+
body: wireBody,
|
|
218
296
|
timeoutMs: options.timeoutMs ?? this.timeoutMs,
|
|
219
297
|
responseType: options.responseType,
|
|
220
298
|
maxBytes: options.maxBytes,
|
|
@@ -311,14 +389,8 @@ export class EbayApiClient {
|
|
|
311
389
|
})));
|
|
312
390
|
}
|
|
313
391
|
if (error.status != null) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
method,
|
|
317
|
-
url,
|
|
318
|
-
status: error.status,
|
|
319
|
-
message: `eBay API Error: ${this.ebayErrorDetail(error.data) ?? error.message}${this.ebayErrorBody(error.data)}`,
|
|
320
|
-
cause: error,
|
|
321
|
-
}));
|
|
392
|
+
const statusError = this.httpStatusError(error, context);
|
|
393
|
+
return Effect.fail(statusError);
|
|
322
394
|
}
|
|
323
395
|
return Effect.fail(clientRequestError({
|
|
324
396
|
kind: 'transport',
|
|
@@ -328,6 +400,29 @@ export class EbayApiClient {
|
|
|
328
400
|
cause: error,
|
|
329
401
|
}));
|
|
330
402
|
}
|
|
403
|
+
httpStatusError(error, context) {
|
|
404
|
+
const { method, url, options } = context;
|
|
405
|
+
const detail = this.ebayErrorDetail(error.data);
|
|
406
|
+
// Without credentials, a signature rejection is a setup problem: show setup guidance.
|
|
407
|
+
if (options.signed && isSignatureFailure(detail) && this.config.signing === undefined) {
|
|
408
|
+
return clientRequestError({
|
|
409
|
+
kind: 'signing',
|
|
410
|
+
method,
|
|
411
|
+
url,
|
|
412
|
+
status: error.status,
|
|
413
|
+
message: `${detail?.replace(TRAILING_PERIOD_PATTERN, '.')}${SIGNING_SETUP_HINT}`,
|
|
414
|
+
cause: error,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return clientRequestError({
|
|
418
|
+
kind: 'httpStatus',
|
|
419
|
+
method,
|
|
420
|
+
url,
|
|
421
|
+
status: error.status,
|
|
422
|
+
message: `eBay API Error: ${detail || error.message}${this.ebayErrorBody(error.data)}`,
|
|
423
|
+
cause: error,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
331
426
|
/**
|
|
332
427
|
* Make a GET request to eBay API
|
|
333
428
|
*/
|
|
@@ -340,6 +435,7 @@ export class EbayApiClient {
|
|
|
340
435
|
timeoutMs: config?.timeoutMs,
|
|
341
436
|
maxBytes: config?.maxBytes,
|
|
342
437
|
tokenType: config?.tokenType,
|
|
438
|
+
signed: config?.signed,
|
|
343
439
|
});
|
|
344
440
|
}
|
|
345
441
|
/**
|
|
@@ -355,6 +451,7 @@ export class EbayApiClient {
|
|
|
355
451
|
timeoutMs: config?.timeoutMs,
|
|
356
452
|
maxBytes: config?.maxBytes,
|
|
357
453
|
tokenType: config?.tokenType,
|
|
454
|
+
signed: config?.signed,
|
|
358
455
|
});
|
|
359
456
|
}
|
|
360
457
|
/**
|
|
@@ -377,6 +474,7 @@ export class EbayApiClient {
|
|
|
377
474
|
timeoutMs: config?.timeoutMs,
|
|
378
475
|
maxBytes: config?.maxBytes,
|
|
379
476
|
tokenType: config?.tokenType,
|
|
477
|
+
signed: config?.signed,
|
|
380
478
|
});
|
|
381
479
|
}
|
|
382
480
|
/**
|
|
@@ -399,6 +497,7 @@ export class EbayApiClient {
|
|
|
399
497
|
timeoutMs: config?.timeoutMs,
|
|
400
498
|
maxBytes: config?.maxBytes,
|
|
401
499
|
tokenType: config?.tokenType,
|
|
500
|
+
signed: config?.signed,
|
|
402
501
|
});
|
|
403
502
|
}
|
|
404
503
|
/**
|
|
@@ -414,6 +513,7 @@ export class EbayApiClient {
|
|
|
414
513
|
timeoutMs: config?.timeoutMs,
|
|
415
514
|
maxBytes: config?.maxBytes,
|
|
416
515
|
tokenType: config?.tokenType,
|
|
516
|
+
signed: config?.signed,
|
|
417
517
|
});
|
|
418
518
|
}
|
|
419
519
|
/**
|
|
@@ -437,6 +537,7 @@ export class EbayApiClient {
|
|
|
437
537
|
timeoutMs: config?.timeoutMs,
|
|
438
538
|
maxBytes: config?.maxBytes,
|
|
439
539
|
tokenType: config?.tokenType,
|
|
540
|
+
signed: config?.signed,
|
|
440
541
|
});
|
|
441
542
|
}
|
|
442
543
|
/**
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, sign } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* eBay Digital Signatures for APIs (RFC 9421 HTTP Message Signatures).
|
|
4
|
+
*
|
|
5
|
+
* eBay requires EU- and UK-domiciled sellers to sign calls to the Finances API,
|
|
6
|
+
* Fulfillment `issueRefund`, Trading `GetAccount`, and several Post-Order refund
|
|
7
|
+
* and cancellation calls. A signed request carries four extra headers:
|
|
8
|
+
*
|
|
9
|
+
* - `x-ebay-signature-key` — the JWE returned by Key Management `createSigningKey`
|
|
10
|
+
* - `Content-Digest` — `sha-256=:<base64>:` of the body, only when a body is sent
|
|
11
|
+
* - `Signature-Input` — the covered components and the `created` timestamp
|
|
12
|
+
* - `Signature` — the Ed25519 or RSA (PKCS#1 v1.5, SHA-256) signature over the signature base
|
|
13
|
+
*
|
|
14
|
+
* @see https://developer.ebay.com/develop/guides/digital-signatures-for-apis
|
|
15
|
+
* @see https://www.rfc-editor.org/rfc/rfc9421
|
|
16
|
+
*/
|
|
17
|
+
/** Label used for the single signature eBay expects. */
|
|
18
|
+
const SIGNATURE_LABEL = 'sig1';
|
|
19
|
+
/** Header name eBay reads the signing key JWE from. */
|
|
20
|
+
const SIGNATURE_KEY_HEADER = 'x-ebay-signature-key';
|
|
21
|
+
const PEM_PATTERN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
22
|
+
/**
|
|
23
|
+
* Sign the signature base with Ed25519, or RSASSA-PKCS1-v1_5 with SHA-256.
|
|
24
|
+
*/
|
|
25
|
+
const signBase = (signatureBase, key) => {
|
|
26
|
+
const signatureBaseBytes = Buffer.from(signatureBase, 'utf8');
|
|
27
|
+
const digest = key.asymmetricKeyType === 'ed25519' ? null : 'sha256';
|
|
28
|
+
return sign(digest, signatureBaseBytes, key).toString('base64');
|
|
29
|
+
};
|
|
30
|
+
/** Lower-cased `@authority`: host plus any non-default port (`URL.host` already omits defaults). */
|
|
31
|
+
const authorityOf = (url) => url.host.toLowerCase();
|
|
32
|
+
/** Raised when the configured signing private key cannot be used. */
|
|
33
|
+
export class EbaySigningKeyError extends Error {
|
|
34
|
+
name = 'EbaySigningKeyError';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse the configured private key into a Node `KeyObject`.
|
|
38
|
+
*
|
|
39
|
+
* eBay's `createSigningKey` returns the private key as base64-encoded PKCS#8 DER
|
|
40
|
+
* without PEM armour; a PEM block (PKCS#8 or PKCS#1 RSA) is accepted as well so
|
|
41
|
+
* keys converted with openssl keep working. Escaped `\n` sequences — common when a
|
|
42
|
+
* PEM key is pasted into a single-line env var or JSON config — are unescaped first.
|
|
43
|
+
*
|
|
44
|
+
* @param privateKey - Base64 PKCS#8 DER or PEM private key.
|
|
45
|
+
* @returns The parsed key object.
|
|
46
|
+
* @throws {EbaySigningKeyError} When the key cannot be parsed or is not Ed25519/RSA.
|
|
47
|
+
*/
|
|
48
|
+
export const parseSigningPrivateKey = (privateKey) => {
|
|
49
|
+
const trimmed = privateKey.trim().replaceAll('\\n', '\n');
|
|
50
|
+
let key;
|
|
51
|
+
try {
|
|
52
|
+
key = PEM_PATTERN.test(trimmed)
|
|
53
|
+
? createPrivateKey(trimmed)
|
|
54
|
+
: createPrivateKey({
|
|
55
|
+
key: Buffer.from(trimmed.replaceAll(/\s/g, ''), 'base64'),
|
|
56
|
+
format: 'der',
|
|
57
|
+
type: 'pkcs8',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (cause) {
|
|
61
|
+
throw new EbaySigningKeyError('EBAY_SIGNING_PRIVATE_KEY could not be parsed. Use the privateKey value returned by ebay_create_signing_key (base64 PKCS#8) or a PEM private key.', { cause });
|
|
62
|
+
}
|
|
63
|
+
if (key.asymmetricKeyType !== 'ed25519' && key.asymmetricKeyType !== 'rsa') {
|
|
64
|
+
throw new EbaySigningKeyError(`EBAY_SIGNING_PRIVATE_KEY is a ${key.asymmetricKeyType ?? 'unknown'} key; eBay digital signatures accept only ED25519 or RSA keys.`);
|
|
65
|
+
}
|
|
66
|
+
return key;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Compute the RFC 9530 `Content-Digest` header value for a body.
|
|
70
|
+
*
|
|
71
|
+
* @param bodyBytes - Exact body bytes (strings are encoded as UTF-8).
|
|
72
|
+
* @returns The header value, e.g. `sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:`.
|
|
73
|
+
*/
|
|
74
|
+
export const computeContentDigest = (bodyBytes) => `sha-256=:${createHash('sha256').update(bodyBytes).digest('base64')}:`;
|
|
75
|
+
/**
|
|
76
|
+
* Build the RFC 9421 signature base for the components eBay covers.
|
|
77
|
+
*
|
|
78
|
+
* @param components - Ordered `[name, value]` pairs of covered components.
|
|
79
|
+
* @param signatureParams - The serialized inner list and parameters, e.g.
|
|
80
|
+
* `("x-ebay-signature-key" "@method" "@path" "@authority");created=1658440308`.
|
|
81
|
+
* @returns The newline-joined signature base.
|
|
82
|
+
*/
|
|
83
|
+
export const buildSignatureBase = (components, signatureParams) => [
|
|
84
|
+
...components.map(([name, value]) => `"${name}": ${value}`),
|
|
85
|
+
`"@signature-params": ${signatureParams}`,
|
|
86
|
+
].join('\n');
|
|
87
|
+
/**
|
|
88
|
+
* Produce the eBay digital-signature headers for one request attempt.
|
|
89
|
+
*
|
|
90
|
+
* The signature covers `content-digest` (only when a body is present),
|
|
91
|
+
* `x-ebay-signature-key`, `@method`, `@path`, and `@authority`, matching eBay's
|
|
92
|
+
* documented `Signature-Input`. Call this once per attempt so retries carry a
|
|
93
|
+
* fresh `created` timestamp.
|
|
94
|
+
*
|
|
95
|
+
* @param request - Method, the URL eBay receives, and the exact body bytes.
|
|
96
|
+
* @param jwe - The `jwe` value from `createSigningKey`, sent in `x-ebay-signature-key`.
|
|
97
|
+
* @param key - Parsed private key (see {@link parseSigningPrivateKey}).
|
|
98
|
+
* @param now - Clock override in milliseconds, for tests.
|
|
99
|
+
* @returns Headers to merge into the outgoing request.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const key = parseSigningPrivateKey(privateKey);
|
|
104
|
+
* const headers = signEbayRequest({ method: 'GET', url }, jwe, key);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export const signEbayRequest = (request, jwe, key, now = Date.now()) => {
|
|
108
|
+
const url = new URL(request.url);
|
|
109
|
+
const created = Math.floor(now / 1000);
|
|
110
|
+
const headers = { [SIGNATURE_KEY_HEADER]: jwe };
|
|
111
|
+
const components = [];
|
|
112
|
+
if (request.bodyBytes !== undefined) {
|
|
113
|
+
const contentDigest = computeContentDigest(request.bodyBytes);
|
|
114
|
+
headers['Content-Digest'] = contentDigest;
|
|
115
|
+
components.push(['content-digest', contentDigest]);
|
|
116
|
+
}
|
|
117
|
+
components.push([SIGNATURE_KEY_HEADER, jwe], ['@method', request.method.toUpperCase()], ['@path', url.pathname], ['@authority', authorityOf(url)]);
|
|
118
|
+
const signatureParams = `(${components.map(([name]) => `"${name}"`).join(' ')});created=${created}`;
|
|
119
|
+
const signatureBase = buildSignatureBase(components, signatureParams);
|
|
120
|
+
const signature = signBase(signatureBase, key);
|
|
121
|
+
headers['Signature-Input'] = `${SIGNATURE_LABEL}=${signatureParams}`;
|
|
122
|
+
headers.Signature = `${SIGNATURE_LABEL}=:${signature}:`;
|
|
123
|
+
return headers;
|
|
124
|
+
};
|
|
@@ -19,13 +19,13 @@ export class FeedbackApi {
|
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
21
|
* const awaiting = await Effect.runPromise(
|
|
22
|
-
* feedbackApi.
|
|
22
|
+
* feedbackApi.getItemsAwaitingFeedback({ filter: 'userRole:SELLER', limit: 25 }),
|
|
23
23
|
* );
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
26
|
* @see https://developer.ebay.com/api-docs/commerce/feedback/resources/awaiting_feedback/methods/getItemsAwaitingFeedback
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
getItemsAwaitingFeedback = (input = {}) => {
|
|
29
29
|
const client = this.client;
|
|
30
30
|
const path = `${this.basePath}/awaiting_feedback`;
|
|
31
31
|
return Effect.gen(function* () {
|
|
@@ -128,13 +128,13 @@ export class FeedbackApi {
|
|
|
128
128
|
* @example
|
|
129
129
|
* ```ts
|
|
130
130
|
* const feedback = await Effect.runPromise(
|
|
131
|
-
* feedbackApi.
|
|
131
|
+
* feedbackApi.leaveFeedback({ commentText: 'Great buyer', commentType: 'POSITIVE' }),
|
|
132
132
|
* );
|
|
133
133
|
* ```
|
|
134
134
|
*
|
|
135
135
|
* @see https://developer.ebay.com/api-docs/commerce/feedback/resources/feedback/methods/leaveFeedback
|
|
136
136
|
*/
|
|
137
|
-
|
|
137
|
+
leaveFeedback = (feedbackData) => {
|
|
138
138
|
const client = this.client;
|
|
139
139
|
const path = `${this.basePath}/feedback`;
|
|
140
140
|
return Effect.gen(function* () {
|