ebay-mcp-remote-edition 1.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +755 -0
  3. package/build/api/account-management/account.js +301 -0
  4. package/build/api/analytics-and-report/analytics.js +102 -0
  5. package/build/api/client-trading.js +96 -0
  6. package/build/api/client.js +173 -0
  7. package/build/api/communication/feedback.js +119 -0
  8. package/build/api/communication/message.js +131 -0
  9. package/build/api/communication/negotiation.js +97 -0
  10. package/build/api/communication/notification.js +373 -0
  11. package/build/api/developer/developer.js +81 -0
  12. package/build/api/index.js +109 -0
  13. package/build/api/listing-management/inventory.js +640 -0
  14. package/build/api/listing-metadata/metadata.js +485 -0
  15. package/build/api/listing-metadata/taxonomy.js +58 -0
  16. package/build/api/marketing-and-promotions/marketing.js +768 -0
  17. package/build/api/marketing-and-promotions/recommendation.js +32 -0
  18. package/build/api/order-management/dispute.js +69 -0
  19. package/build/api/order-management/fulfillment.js +89 -0
  20. package/build/api/other/compliance.js +47 -0
  21. package/build/api/other/edelivery.js +219 -0
  22. package/build/api/other/identity.js +24 -0
  23. package/build/api/other/translation.js +22 -0
  24. package/build/api/other/vero.js +48 -0
  25. package/build/api/trading/trading.js +78 -0
  26. package/build/auth/kv-store.js +40 -0
  27. package/build/auth/multi-user-store.js +120 -0
  28. package/build/auth/oauth-metadata.js +59 -0
  29. package/build/auth/oauth-middleware.js +99 -0
  30. package/build/auth/oauth-types.js +4 -0
  31. package/build/auth/oauth.js +235 -0
  32. package/build/auth/scope-utils.js +304 -0
  33. package/build/auth/token-store.js +46 -0
  34. package/build/auth/token-verifier.js +172 -0
  35. package/build/config/environment.js +297 -0
  36. package/build/index.d.ts +1 -0
  37. package/build/index.js +129 -0
  38. package/build/schemas/account-management/account.js +375 -0
  39. package/build/schemas/analytics/analytics.js +191 -0
  40. package/build/schemas/communication/messages.js +345 -0
  41. package/build/schemas/fulfillment/orders.js +338 -0
  42. package/build/schemas/index.js +68 -0
  43. package/build/schemas/inventory-management/inventory.js +471 -0
  44. package/build/schemas/marketing/marketing.js +1103 -0
  45. package/build/schemas/metadata/metadata.js +618 -0
  46. package/build/schemas/other/other-apis.js +390 -0
  47. package/build/schemas/taxonomy/taxonomy.js +575 -0
  48. package/build/scripts/auto-setup.js +364 -0
  49. package/build/scripts/dev-sync.js +512 -0
  50. package/build/scripts/diagnostics.js +301 -0
  51. package/build/scripts/download-specs.js +116 -0
  52. package/build/scripts/interactive-setup.js +757 -0
  53. package/build/scripts/setup.js +1515 -0
  54. package/build/scripts/update-api-status-doc.js +44 -0
  55. package/build/server-http.d.ts +1 -0
  56. package/build/server-http.js +581 -0
  57. package/build/tools/definitions/account-with-schemas.js +170 -0
  58. package/build/tools/definitions/account.js +428 -0
  59. package/build/tools/definitions/analytics.js +66 -0
  60. package/build/tools/definitions/communication.js +394 -0
  61. package/build/tools/definitions/developer.js +195 -0
  62. package/build/tools/definitions/fulfillment.js +326 -0
  63. package/build/tools/definitions/index.js +41 -0
  64. package/build/tools/definitions/inventory.js +464 -0
  65. package/build/tools/definitions/marketing.js +1486 -0
  66. package/build/tools/definitions/metadata.js +188 -0
  67. package/build/tools/definitions/other.js +309 -0
  68. package/build/tools/definitions/taxonomy.js +64 -0
  69. package/build/tools/definitions/token-management.js +148 -0
  70. package/build/tools/definitions/trading.js +71 -0
  71. package/build/tools/index.js +1200 -0
  72. package/build/tools/schemas.js +667 -0
  73. package/build/tools/tool-definitions.js +3534 -0
  74. package/build/types/application-settings/developerAnalyticsV1BetaOas3.js +5 -0
  75. package/build/types/application-settings/developerClientRegistrationV1Oas3.js +5 -0
  76. package/build/types/application-settings/developerKeyManagementV1Oas3.js +5 -0
  77. package/build/types/ebay-enums.js +1330 -0
  78. package/build/types/ebay.js +123 -0
  79. package/build/types/index.js +10 -0
  80. package/build/types/sell-apps/account-management/sellAccountV1Oas3.js +5 -0
  81. package/build/types/sell-apps/analytics-and-report/sellAnalyticsV1Oas3.js +5 -0
  82. package/build/types/sell-apps/communication/commerceFeedbackV1BetaOas3.js +5 -0
  83. package/build/types/sell-apps/communication/commerceMessageV1Oas3.js +5 -0
  84. package/build/types/sell-apps/communication/commerceNotificationV1Oas3.js +5 -0
  85. package/build/types/sell-apps/communication/sellNegotiationV1Oas3.js +5 -0
  86. package/build/types/sell-apps/listing-management/sellInventoryV1Oas3.js +5 -0
  87. package/build/types/sell-apps/listing-metadata/sellMetadataV1Oas3.js +5 -0
  88. package/build/types/sell-apps/markeitng-and-promotions/sellMarketingV1Oas3.js +5 -0
  89. package/build/types/sell-apps/markeitng-and-promotions/sellRecommendationV1Oas3.js +5 -0
  90. package/build/types/sell-apps/order-management/sellFulfillmentV1Oas3.js +5 -0
  91. package/build/types/sell-apps/other-apis/commerceIdentityV1Oas3.js +5 -0
  92. package/build/types/sell-apps/other-apis/commerceTranslationV1BetaOas3.js +5 -0
  93. package/build/types/sell-apps/other-apis/commerceVeroV1Oas3.js +5 -0
  94. package/build/types/sell-apps/other-apis/sellComplianceV1Oas3.js +5 -0
  95. package/build/types/sell-apps/other-apis/sellEdeliveryInternationalShippingOas3.js +5 -0
  96. package/build/types/sell-apps/other-apis/sellMarketingV1Oas3.js +5 -0
  97. package/build/types/sell-apps/other-apis/sellRecommendationV1Oas3.js +5 -0
  98. package/build/utils/account-management/account.js +831 -0
  99. package/build/utils/api-status-feed.js +83 -0
  100. package/build/utils/communication/feedback.js +216 -0
  101. package/build/utils/communication/message.js +242 -0
  102. package/build/utils/communication/negotiation.js +150 -0
  103. package/build/utils/communication/notification.js +369 -0
  104. package/build/utils/date-converter.js +160 -0
  105. package/build/utils/llm-client-detector.js +758 -0
  106. package/build/utils/logger.js +198 -0
  107. package/build/utils/oauth-helper.js +315 -0
  108. package/build/utils/order-management/dispute.js +369 -0
  109. package/build/utils/order-management/fulfillment.js +205 -0
  110. package/build/utils/other/compliance.js +76 -0
  111. package/build/utils/other/edelivery.js +241 -0
  112. package/build/utils/other/identity.js +13 -0
  113. package/build/utils/other/translation.js +41 -0
  114. package/build/utils/other/vero.js +90 -0
  115. package/build/utils/scope-helper.js +207 -0
  116. package/build/utils/security-checker.js +248 -0
  117. package/build/utils/setup-validator.js +305 -0
  118. package/build/utils/token-utils.js +40 -0
  119. package/build/utils/version.js +56 -0
  120. package/docs/auth/production_scopes.json +111 -0
  121. package/docs/auth/sandbox_scopes.json +142 -0
  122. package/package.json +122 -0
  123. package/public/icons/1024x1024.png +0 -0
  124. package/public/icons/128x128.png +0 -0
  125. package/public/icons/16x16.png +0 -0
  126. package/public/icons/256x256.png +0 -0
  127. package/public/icons/32x32.png +0 -0
  128. package/public/icons/48x48.png +0 -0
  129. package/public/icons/512x512.png +0 -0
@@ -0,0 +1,120 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { CloudflareKVStore } from '../auth/kv-store.js';
3
+ export class MultiUserAuthStore {
4
+ kv = new CloudflareKVStore();
5
+ stateKey(state) {
6
+ return `oauth_state:${state}`;
7
+ }
8
+ userTokenKey(userId, environment) {
9
+ return `user:${userId}:env:${environment}:tokens`;
10
+ }
11
+ sessionKey(sessionToken) {
12
+ return `session:${sessionToken}`;
13
+ }
14
+ async createOAuthState(environment, returnTo, mcpContext) {
15
+ const state = randomUUID();
16
+ const record = {
17
+ state,
18
+ environment,
19
+ createdAt: new Date().toISOString(),
20
+ returnTo,
21
+ ...mcpContext,
22
+ };
23
+ await this.kv.put(this.stateKey(state), record, 15 * 60);
24
+ return record;
25
+ }
26
+ async consumeOAuthState(state) {
27
+ const key = this.stateKey(state);
28
+ const record = await this.kv.get(key);
29
+ if (record) {
30
+ await this.kv.delete(key);
31
+ }
32
+ return record;
33
+ }
34
+ async saveUserTokens(userId, environment, tokenData) {
35
+ const record = {
36
+ userId,
37
+ environment,
38
+ tokenData,
39
+ updatedAt: new Date().toISOString(),
40
+ };
41
+ await this.kv.put(this.userTokenKey(userId, environment), record);
42
+ }
43
+ async getUserTokens(userId, environment) {
44
+ return await this.kv.get(this.userTokenKey(userId, environment));
45
+ }
46
+ async createSession(userId, environment) {
47
+ const sessionToken = randomUUID() + randomUUID();
48
+ const now = new Date().toISOString();
49
+ const record = {
50
+ sessionToken,
51
+ userId,
52
+ environment,
53
+ createdAt: now,
54
+ lastUsedAt: now,
55
+ };
56
+ await this.kv.put(this.sessionKey(sessionToken), record);
57
+ return record;
58
+ }
59
+ async getSession(sessionToken) {
60
+ return await this.kv.get(this.sessionKey(sessionToken));
61
+ }
62
+ async touchSession(sessionToken) {
63
+ const record = await this.getSession(sessionToken);
64
+ if (!record || record.revokedAt) {
65
+ return;
66
+ }
67
+ record.lastUsedAt = new Date().toISOString();
68
+ await this.kv.put(this.sessionKey(sessionToken), record);
69
+ }
70
+ async revokeSession(sessionToken) {
71
+ const record = await this.getSession(sessionToken);
72
+ if (!record) {
73
+ return;
74
+ }
75
+ record.revokedAt = new Date().toISOString();
76
+ await this.kv.put(this.sessionKey(sessionToken), record);
77
+ }
78
+ async deleteSession(sessionToken) {
79
+ await this.kv.delete(this.sessionKey(sessionToken));
80
+ }
81
+ // ── RFC 7591 Dynamic Client Registration ──────────────────────────────────
82
+ async registerClient(redirectUris, clientName) {
83
+ const clientId = randomUUID();
84
+ const record = {
85
+ clientId,
86
+ redirectUris,
87
+ clientName,
88
+ createdAt: new Date().toISOString(),
89
+ };
90
+ await this.kv.put(`client:${clientId}`, record);
91
+ return record;
92
+ }
93
+ async getClient(clientId) {
94
+ return await this.kv.get(`client:${clientId}`);
95
+ }
96
+ // ── MCP Authorization Code (short-lived, PKCE-protected) ─────────────────
97
+ async createAuthCode(clientId, redirectUri, codeChallenge, codeChallengeMethod, userId, environment) {
98
+ const code = randomUUID() + randomUUID();
99
+ const record = {
100
+ code,
101
+ clientId,
102
+ redirectUri,
103
+ codeChallenge,
104
+ codeChallengeMethod,
105
+ userId,
106
+ environment,
107
+ createdAt: new Date().toISOString(),
108
+ };
109
+ await this.kv.put(`auth_code:${code}`, record, 10 * 60); // 10 min TTL
110
+ return record;
111
+ }
112
+ async consumeAuthCode(code) {
113
+ const key = `auth_code:${code}`;
114
+ const record = await this.kv.get(key);
115
+ if (record) {
116
+ await this.kv.delete(key);
117
+ }
118
+ return record;
119
+ }
120
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * OAuth metadata endpoints for MCP server
3
+ * Implements RFC 9728 Protected Resource Metadata
4
+ */
5
+ import { Router as createRouter } from 'express';
6
+ /**
7
+ * Create Express router with OAuth metadata endpoints
8
+ */
9
+ export function createMetadataRouter(config) {
10
+ const router = createRouter();
11
+ // RFC 9728: Protected Resource Metadata endpoint
12
+ // Path: /.well-known/oauth-protected-resource
13
+ router.get('/.well-known/oauth-protected-resource', (req, res) => {
14
+ const authServers = typeof config.authServerMetadata === 'string'
15
+ ? [config.authServerMetadata]
16
+ : [config.authServerMetadata.issuer];
17
+ const metadata = {
18
+ resource: config.resourceServerUrl,
19
+ authorization_servers: authServers,
20
+ scopes_supported: config.scopesSupported,
21
+ };
22
+ if (config.resourceDocumentation) {
23
+ metadata.resource_documentation = config.resourceDocumentation;
24
+ }
25
+ res.json(metadata);
26
+ });
27
+ // Optional: Server info endpoint for debugging
28
+ router.get('/.well-known/mcp-server-info', (req, res) => {
29
+ const serverInfo = {
30
+ name: config.resourceName || 'MCP Resource Server',
31
+ version: '1.0.0',
32
+ resource_url: config.resourceServerUrl,
33
+ authorization_required: true,
34
+ scopes_supported: config.scopesSupported,
35
+ documentation: config.resourceDocumentation,
36
+ };
37
+ // Add eBay-specific information if provided
38
+ if (config.ebayEnvironment) {
39
+ serverInfo.ebay = {
40
+ environment: config.ebayEnvironment,
41
+ base_url: config.ebayEnvironment === 'production'
42
+ ? 'https://api.ebay.com'
43
+ : 'https://api.sandbox.ebay.com',
44
+ scopes: config.ebayScopes || [],
45
+ note: 'MCP OAuth scopes (scopes_supported) are separate from eBay API OAuth scopes (ebay.scopes)',
46
+ };
47
+ }
48
+ res.json(serverInfo);
49
+ });
50
+ return router;
51
+ }
52
+ /**
53
+ * Helper to get Protected Resource Metadata URL from server URL
54
+ */
55
+ export function getProtectedResourceMetadataUrl(serverUrl) {
56
+ const url = new URL(serverUrl);
57
+ url.pathname = '/.well-known/oauth-protected-resource';
58
+ return url.toString();
59
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * OAuth 2.1 middleware for Express
3
+ * Implements RFC 6750 Bearer Token authentication
4
+ */
5
+ /**
6
+ * Create Bearer token authentication middleware
7
+ */
8
+ export function createBearerAuthMiddleware(config) {
9
+ const realm = config.realm || 'mcp';
10
+ return async (req, res, next) => {
11
+ try {
12
+ // Extract token from Authorization header
13
+ const authHeader = req.headers.authorization;
14
+ if (!authHeader) {
15
+ sendUnauthorized(res, realm, config.resourceMetadataUrl, {
16
+ error: 'invalid_token',
17
+ error_description: 'No authorization header provided',
18
+ });
19
+ return;
20
+ }
21
+ // Check Bearer scheme
22
+ const parts = authHeader.split(' ');
23
+ if (parts.length !== 2 || parts[0] !== 'Bearer') {
24
+ sendUnauthorized(res, realm, config.resourceMetadataUrl, {
25
+ error: 'invalid_token',
26
+ error_description: 'Invalid authorization header format. Expected: Bearer <token>',
27
+ });
28
+ return;
29
+ }
30
+ const token = parts[1];
31
+ // Verify token
32
+ try {
33
+ const verifiedToken = await config.verifier.verifyToken(token);
34
+ req.auth = verifiedToken;
35
+ next();
36
+ }
37
+ catch (error) {
38
+ const errorMessage = error instanceof Error ? error.message : 'Token verification failed';
39
+ sendUnauthorized(res, realm, config.resourceMetadataUrl, {
40
+ error: 'invalid_token',
41
+ error_description: errorMessage,
42
+ });
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error('OAuth middleware error:', error);
47
+ res.status(500).json({
48
+ error: 'server_error',
49
+ error_description: 'Internal server error during authentication',
50
+ });
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Send 401 Unauthorized response with RFC 6750 compliant WWW-Authenticate header
56
+ */
57
+ function sendUnauthorized(res, realm, resourceMetadataUrl, challenge) {
58
+ // Build WWW-Authenticate header per RFC 6750
59
+ let authenticateValue = `Bearer realm="${realm}", resource_metadata="${resourceMetadataUrl}"`;
60
+ if (challenge.error) {
61
+ authenticateValue += `, error="${challenge.error}"`;
62
+ }
63
+ if (challenge.error_description) {
64
+ authenticateValue += `, error_description="${challenge.error_description}"`;
65
+ }
66
+ if (challenge.scope) {
67
+ authenticateValue += `, scope="${challenge.scope}"`;
68
+ }
69
+ res.setHeader('WWW-Authenticate', authenticateValue);
70
+ res.status(401).json({
71
+ error: challenge.error || 'unauthorized',
72
+ error_description: challenge.error_description || 'Authorization required',
73
+ });
74
+ }
75
+ /**
76
+ * Optional middleware to check specific scopes
77
+ */
78
+ export function requireScopes(requiredScopes) {
79
+ return (req, res, next) => {
80
+ if (!req.auth) {
81
+ res.status(401).json({
82
+ error: 'unauthorized',
83
+ error_description: 'No authentication information found',
84
+ });
85
+ return;
86
+ }
87
+ const hasRequiredScopes = requiredScopes.every((scope) => req.auth.scopes.includes(scope));
88
+ if (!hasRequiredScopes) {
89
+ res.status(403).json({
90
+ error: 'insufficient_scope',
91
+ error_description: `Missing required scopes: ${requiredScopes.join(', ')}`,
92
+ required_scopes: requiredScopes,
93
+ provided_scopes: req.auth.scopes,
94
+ });
95
+ return;
96
+ }
97
+ next();
98
+ };
99
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * OAuth 2.1 types for MCP server authorization
3
+ */
4
+ export {};
@@ -0,0 +1,235 @@
1
+ import axios from 'axios';
2
+ import { getBaseUrl } from '../config/environment.js';
3
+ import { MultiUserAuthStore } from '../auth/multi-user-store.js';
4
+ export class EbayOAuthClient {
5
+ config;
6
+ context;
7
+ appAccessToken = null;
8
+ appAccessTokenExpiry = 0;
9
+ userTokens = null;
10
+ authStore = new MultiUserAuthStore();
11
+ constructor(config, context) {
12
+ this.config = config;
13
+ this.context = context;
14
+ }
15
+ async initialize() {
16
+ if (this.context?.userId && this.context.environment) {
17
+ const stored = await this.authStore.getUserTokens(this.context.userId, this.context.environment);
18
+ if (stored?.tokenData) {
19
+ this.userTokens = stored.tokenData;
20
+ return;
21
+ }
22
+ }
23
+ // Fallback: load from EBAY_USER_REFRESH_TOKEN environment variable
24
+ const envRefreshToken = process.env.EBAY_USER_REFRESH_TOKEN;
25
+ if (envRefreshToken) {
26
+ try {
27
+ const authUrl = `${getBaseUrl(this.config.environment)}/identity/v1/oauth2/token`;
28
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
29
+ const response = await axios.post(authUrl, new URLSearchParams({
30
+ grant_type: 'refresh_token',
31
+ refresh_token: envRefreshToken,
32
+ }).toString(), {
33
+ headers: {
34
+ 'Content-Type': 'application/x-www-form-urlencoded',
35
+ Authorization: `Basic ${credentials}`,
36
+ },
37
+ });
38
+ const tokenData = response.data;
39
+ const now = Date.now();
40
+ this.userTokens = {
41
+ clientId: this.config.clientId,
42
+ clientSecret: this.config.clientSecret,
43
+ redirectUri: this.config.redirectUri,
44
+ userAccessToken: tokenData.access_token,
45
+ userRefreshToken: tokenData.refresh_token || envRefreshToken,
46
+ tokenType: tokenData.token_type,
47
+ userAccessTokenExpiry: now + tokenData.expires_in * 1000,
48
+ userRefreshTokenExpiry: tokenData.refresh_token_expires_in
49
+ ? now + tokenData.refresh_token_expires_in * 1000
50
+ : now + 18 * 30 * 24 * 60 * 60 * 1000,
51
+ scope: tokenData.scope,
52
+ };
53
+ }
54
+ catch {
55
+ // If refresh fails, leave userTokens as null
56
+ }
57
+ }
58
+ }
59
+ hasUserTokens() {
60
+ return this.userTokens !== null;
61
+ }
62
+ isUserAccessTokenExpired(tokens) {
63
+ return tokens.userAccessTokenExpiry ? Date.now() >= tokens.userAccessTokenExpiry : true;
64
+ }
65
+ isUserRefreshTokenExpired(tokens) {
66
+ return tokens.userRefreshTokenExpiry ? Date.now() >= tokens.userRefreshTokenExpiry : true;
67
+ }
68
+ async persistUserTokens() {
69
+ if (this.context?.userId && this.context.environment && this.userTokens) {
70
+ await this.authStore.saveUserTokens(this.context.userId, this.context.environment, this.userTokens);
71
+ }
72
+ }
73
+ async getAccessToken() {
74
+ if (this.userTokens) {
75
+ if (!this.isUserAccessTokenExpired(this.userTokens)) {
76
+ return this.userTokens.userAccessToken;
77
+ }
78
+ if (!this.isUserRefreshTokenExpired(this.userTokens)) {
79
+ await this.refreshUserToken();
80
+ return this.userTokens.userAccessToken;
81
+ }
82
+ throw new Error('User authorization expired. Re-authorize through browser OAuth and update your MCP connection token.');
83
+ }
84
+ if (this.appAccessToken && Date.now() < this.appAccessTokenExpiry) {
85
+ return this.appAccessToken;
86
+ }
87
+ await this.getOrRefreshAppAccessToken();
88
+ return this.appAccessToken;
89
+ }
90
+ async setUserTokens(accessToken, refreshToken, accessTokenExpiry, refreshTokenExpiry) {
91
+ const now = Date.now();
92
+ this.userTokens = {
93
+ clientId: this.config.clientId,
94
+ clientSecret: this.config.clientSecret,
95
+ redirectUri: this.config.redirectUri,
96
+ userAccessToken: accessToken,
97
+ userRefreshToken: refreshToken,
98
+ tokenType: 'Bearer',
99
+ userAccessTokenExpiry: accessTokenExpiry ?? now + 7200 * 1000,
100
+ userRefreshTokenExpiry: refreshTokenExpiry ?? now + 18 * 30 * 24 * 60 * 60 * 1000,
101
+ };
102
+ await this.persistUserTokens();
103
+ }
104
+ async getOrRefreshAppAccessToken() {
105
+ if (this.appAccessToken && Date.now() < this.appAccessTokenExpiry) {
106
+ return this.appAccessToken;
107
+ }
108
+ const authUrl = `${getBaseUrl(this.config.environment)}/identity/v1/oauth2/token`;
109
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
110
+ const response = await axios.post(authUrl, new URLSearchParams({
111
+ grant_type: 'client_credentials',
112
+ scope: 'https://api.ebay.com/oauth/api_scope',
113
+ }).toString(), {
114
+ headers: {
115
+ 'Content-Type': 'application/x-www-form-urlencoded',
116
+ Authorization: `Basic ${credentials}`,
117
+ },
118
+ });
119
+ this.appAccessToken = response.data.access_token;
120
+ this.appAccessTokenExpiry = Date.now() + (response.data.expires_in - 60) * 1000;
121
+ return this.appAccessToken;
122
+ }
123
+ async exchangeCodeForToken(code) {
124
+ if (!this.config.redirectUri) {
125
+ throw new Error('Redirect URI is required for authorization code exchange');
126
+ }
127
+ const tokenUrl = `${getBaseUrl(this.config.environment)}/identity/v1/oauth2/token`;
128
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
129
+ try {
130
+ const response = await axios.post(tokenUrl, new URLSearchParams({
131
+ grant_type: 'authorization_code',
132
+ code,
133
+ redirect_uri: this.config.redirectUri,
134
+ }).toString(), {
135
+ headers: {
136
+ 'Content-Type': 'application/x-www-form-urlencoded',
137
+ Authorization: `Basic ${credentials}`,
138
+ },
139
+ });
140
+ const tokenData = response.data;
141
+ const now = Date.now();
142
+ this.userTokens = {
143
+ clientId: this.config.clientId,
144
+ clientSecret: this.config.clientSecret,
145
+ redirectUri: this.config.redirectUri,
146
+ userAccessToken: tokenData.access_token,
147
+ userRefreshToken: tokenData.refresh_token,
148
+ tokenType: tokenData.token_type,
149
+ userAccessTokenExpiry: now + tokenData.expires_in * 1000,
150
+ userRefreshTokenExpiry: now + tokenData.refresh_token_expires_in * 1000,
151
+ scope: tokenData.scope,
152
+ };
153
+ await this.persistUserTokens();
154
+ return tokenData;
155
+ }
156
+ catch (error) {
157
+ if (axios.isAxiosError(error) && error.response?.data) {
158
+ const data = error.response.data;
159
+ if (data.error_description) {
160
+ throw new Error(data.error_description);
161
+ }
162
+ if (data.error) {
163
+ throw new Error(data.error);
164
+ }
165
+ }
166
+ throw error;
167
+ }
168
+ }
169
+ async refreshUserToken() {
170
+ if (!this.userTokens) {
171
+ throw new Error('No user tokens available to refresh');
172
+ }
173
+ const authUrl = `${getBaseUrl(this.config.environment)}/identity/v1/oauth2/token`;
174
+ const credentials = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64');
175
+ const response = await axios.post(authUrl, new URLSearchParams({
176
+ grant_type: 'refresh_token',
177
+ refresh_token: this.userTokens.userRefreshToken,
178
+ }).toString(), {
179
+ headers: {
180
+ 'Content-Type': 'application/x-www-form-urlencoded',
181
+ Authorization: `Basic ${credentials}`,
182
+ },
183
+ });
184
+ const tokenData = response.data;
185
+ const now = Date.now();
186
+ this.userTokens = {
187
+ clientId: this.config.clientId,
188
+ clientSecret: this.config.clientSecret,
189
+ redirectUri: this.config.redirectUri,
190
+ userAccessToken: tokenData.access_token,
191
+ userRefreshToken: tokenData.refresh_token || this.userTokens.userRefreshToken,
192
+ tokenType: tokenData.token_type,
193
+ userAccessTokenExpiry: now + tokenData.expires_in * 1000,
194
+ userRefreshTokenExpiry: tokenData.refresh_token_expires_in
195
+ ? now + tokenData.refresh_token_expires_in * 1000
196
+ : this.userTokens.userRefreshTokenExpiry,
197
+ scope: tokenData.scope || this.userTokens.scope,
198
+ };
199
+ await this.persistUserTokens();
200
+ }
201
+ isAuthenticated() {
202
+ if (this.userTokens && !this.isUserAccessTokenExpired(this.userTokens)) {
203
+ return true;
204
+ }
205
+ return this.appAccessToken !== null && Date.now() < this.appAccessTokenExpiry;
206
+ }
207
+ clearAllTokens() {
208
+ this.appAccessToken = null;
209
+ this.appAccessTokenExpiry = 0;
210
+ this.userTokens = null;
211
+ }
212
+ getTokenInfo() {
213
+ const info = {
214
+ hasUserToken: this.userTokens !== null && !this.isUserAccessTokenExpired(this.userTokens),
215
+ hasAppAccessToken: this.appAccessToken !== null && Date.now() < this.appAccessTokenExpiry,
216
+ };
217
+ if (this.userTokens?.scope) {
218
+ const tokenScopes = this.userTokens.scope.split(' ');
219
+ const environmentScopes = ['https://api.ebay.com/oauth/api_scope'];
220
+ const tokenScopeSet = new Set(tokenScopes);
221
+ const missingScopes = environmentScopes.filter((scope) => !tokenScopeSet.has(scope));
222
+ info.scopeInfo = { tokenScopes, environmentScopes, missingScopes };
223
+ }
224
+ return info;
225
+ }
226
+ getUserTokens() {
227
+ return this.userTokens;
228
+ }
229
+ getCachedAppAccessToken() {
230
+ return this.appAccessToken;
231
+ }
232
+ getCachedAppAccessTokenExpiry() {
233
+ return this.appAccessTokenExpiry;
234
+ }
235
+ }