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.
Files changed (44) hide show
  1. package/README.md +34 -16
  2. package/build/api/account-management/account.js +2 -2
  3. package/build/api/account-management/accountV2.js +0 -6
  4. package/build/api/account-management/finances.js +7 -5
  5. package/build/api/client/ebayApiClient.js +111 -10
  6. package/build/api/client/signEbayRequest.js +124 -0
  7. package/build/api/communication/feedback.js +4 -4
  8. package/build/api/developer/developer.js +43 -7
  9. package/build/api/ebaySellerApi.js +0 -4
  10. package/build/api/marketing-and-promotions/campaigns.js +0 -34
  11. package/build/api/order-management/dispute.js +29 -20
  12. package/build/api/order-management/fulfillment.js +2 -1
  13. package/build/api/other/edelivery.js +3 -3
  14. package/build/api/other/translation.js +1 -1
  15. package/build/config/environment.js +31 -3
  16. package/build/schemas/account-management/account.js +1 -1
  17. package/build/schemas/account-management/accountV2.js +3 -4
  18. package/build/schemas/communication/messages.js +2 -2
  19. package/build/schemas/marketing/campaigns.js +0 -21
  20. package/build/schemas/other/otherApis.js +2 -85
  21. package/build/scripts/devSync.js +75 -225
  22. package/build/scripts/specCoverage.js +78 -0
  23. package/build/scripts/specDownload.js +42 -0
  24. package/build/scripts/specRefs.js +2 -3
  25. package/build/scripts/specSources.js +4 -6
  26. package/build/scripts/specTypeNames.js +23 -0
  27. package/build/skills/content.js +2 -2
  28. package/build/tools/categories/account.js +3 -3
  29. package/build/tools/categories/communication.js +5 -5
  30. package/build/tools/categories/developer.js +1 -1
  31. package/build/tools/categories/finances.js +2 -2
  32. package/build/tools/categories/marketing.js +0 -16
  33. package/build/tools/categories/other.js +3 -17
  34. package/build/types/ebay.js +0 -1
  35. package/build/types/ebayEnums.js +0 -40
  36. package/build/ui/card.html +23 -23
  37. package/build/ui/chart.html +22 -22
  38. package/build/ui/stat.html +23 -23
  39. package/build/ui/table.html +21 -21
  40. package/build/utils/communication/feedback.js +3 -3
  41. package/build/utils/errors.js +1 -0
  42. package/package.json +5 -5
  43. package/build/api/other/compliance.js +0 -60
  44. 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. 384 tools for inventory, orders, marketing, and analytics, running locally with your own keys.</strong>
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-384-8957e5?logo=ebay&logoColor=white" alt="384 eBay API 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 **384 tools** spanning **100% of eBay's Sell API surface** (360 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.
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
- - **384 eBay API tools** — 100% coverage of the eBay Sell APIs across inventory, orders, marketing, analytics, metadata, taxonomy, and developer tooling.
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 (Compliance tools remain but report eBay's 2026-03-30 decommission) | "Show my current seller identity details." |
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. eBay requires Digital Signatures on Finances calls for sellers in the EU and
255
- UK, and this server does not sign requests yet, so those sellers get eBay's signature error from
256
- the Finances tools.
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 | 384 tools across 100% of the Sell APIs, ready to call | Build each request from the docs |
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) ≥ 20 and MCP protocol 1.0+ over STDIO (default) or HTTP.
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>384 tools by category (100% Sell API coverage)</strong></summary>
466
+ <summary><strong>380 tools by category (100% Sell API coverage)</strong></summary>
451
467
 
452
- **384 tools**, 100% Sell API coverage, organized by category. Each link points to the tool definitions and handlers in [`src/tools/categories/`](src/tools/categories/):
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 (Compliance tools report eBay decommission) |
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 **384 tools** covering **100% of eBay's Sell APIs** (360 endpoints) to AI assistants — inventory, order fulfillment, marketing, analytics, and developer tools.
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
- 384 tools across 360 unique endpoints — 100% of eBay's Sell APIs.
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 ≥ 20, a free [eBay Developer Account](https://developer.ebay.com/) (Client ID + Client Secret), then run `npm run setup`.
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.getKyc({}));
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
- getKyc = (_input = {}) => requestGetEffect(this.client, `${ACCOUNT_BASE_PATH}/kyc`);
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
- ? undefined
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 { getBaseUrl } from '../../config/environment.js';
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: options.data,
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
- return Effect.fail(clientRequestError({
315
- kind: 'httpStatus',
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.getAwaitingFeedback({ filter: 'userRole:SELLER', limit: 25 }),
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
- getAwaitingFeedback = (input = {}) => {
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.leaveFeedbackForBuyer({ commentText: 'Great buyer', commentType: 'POSITIVE' }),
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
- leaveFeedbackForBuyer = (feedbackData) => {
137
+ leaveFeedback = (feedbackData) => {
138
138
  const client = this.client;
139
139
  const path = `${this.basePath}/feedback`;
140
140
  return Effect.gen(function* () {