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.
- package/LICENSE +21 -0
- package/README.md +755 -0
- package/build/api/account-management/account.js +301 -0
- package/build/api/analytics-and-report/analytics.js +102 -0
- package/build/api/client-trading.js +96 -0
- package/build/api/client.js +173 -0
- package/build/api/communication/feedback.js +119 -0
- package/build/api/communication/message.js +131 -0
- package/build/api/communication/negotiation.js +97 -0
- package/build/api/communication/notification.js +373 -0
- package/build/api/developer/developer.js +81 -0
- package/build/api/index.js +109 -0
- package/build/api/listing-management/inventory.js +640 -0
- package/build/api/listing-metadata/metadata.js +485 -0
- package/build/api/listing-metadata/taxonomy.js +58 -0
- package/build/api/marketing-and-promotions/marketing.js +768 -0
- package/build/api/marketing-and-promotions/recommendation.js +32 -0
- package/build/api/order-management/dispute.js +69 -0
- package/build/api/order-management/fulfillment.js +89 -0
- package/build/api/other/compliance.js +47 -0
- package/build/api/other/edelivery.js +219 -0
- package/build/api/other/identity.js +24 -0
- package/build/api/other/translation.js +22 -0
- package/build/api/other/vero.js +48 -0
- package/build/api/trading/trading.js +78 -0
- package/build/auth/kv-store.js +40 -0
- package/build/auth/multi-user-store.js +120 -0
- package/build/auth/oauth-metadata.js +59 -0
- package/build/auth/oauth-middleware.js +99 -0
- package/build/auth/oauth-types.js +4 -0
- package/build/auth/oauth.js +235 -0
- package/build/auth/scope-utils.js +304 -0
- package/build/auth/token-store.js +46 -0
- package/build/auth/token-verifier.js +172 -0
- package/build/config/environment.js +297 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +129 -0
- package/build/schemas/account-management/account.js +375 -0
- package/build/schemas/analytics/analytics.js +191 -0
- package/build/schemas/communication/messages.js +345 -0
- package/build/schemas/fulfillment/orders.js +338 -0
- package/build/schemas/index.js +68 -0
- package/build/schemas/inventory-management/inventory.js +471 -0
- package/build/schemas/marketing/marketing.js +1103 -0
- package/build/schemas/metadata/metadata.js +618 -0
- package/build/schemas/other/other-apis.js +390 -0
- package/build/schemas/taxonomy/taxonomy.js +575 -0
- package/build/scripts/auto-setup.js +364 -0
- package/build/scripts/dev-sync.js +512 -0
- package/build/scripts/diagnostics.js +301 -0
- package/build/scripts/download-specs.js +116 -0
- package/build/scripts/interactive-setup.js +757 -0
- package/build/scripts/setup.js +1515 -0
- package/build/scripts/update-api-status-doc.js +44 -0
- package/build/server-http.d.ts +1 -0
- package/build/server-http.js +581 -0
- package/build/tools/definitions/account-with-schemas.js +170 -0
- package/build/tools/definitions/account.js +428 -0
- package/build/tools/definitions/analytics.js +66 -0
- package/build/tools/definitions/communication.js +394 -0
- package/build/tools/definitions/developer.js +195 -0
- package/build/tools/definitions/fulfillment.js +326 -0
- package/build/tools/definitions/index.js +41 -0
- package/build/tools/definitions/inventory.js +464 -0
- package/build/tools/definitions/marketing.js +1486 -0
- package/build/tools/definitions/metadata.js +188 -0
- package/build/tools/definitions/other.js +309 -0
- package/build/tools/definitions/taxonomy.js +64 -0
- package/build/tools/definitions/token-management.js +148 -0
- package/build/tools/definitions/trading.js +71 -0
- package/build/tools/index.js +1200 -0
- package/build/tools/schemas.js +667 -0
- package/build/tools/tool-definitions.js +3534 -0
- package/build/types/application-settings/developerAnalyticsV1BetaOas3.js +5 -0
- package/build/types/application-settings/developerClientRegistrationV1Oas3.js +5 -0
- package/build/types/application-settings/developerKeyManagementV1Oas3.js +5 -0
- package/build/types/ebay-enums.js +1330 -0
- package/build/types/ebay.js +123 -0
- package/build/types/index.js +10 -0
- package/build/types/sell-apps/account-management/sellAccountV1Oas3.js +5 -0
- package/build/types/sell-apps/analytics-and-report/sellAnalyticsV1Oas3.js +5 -0
- package/build/types/sell-apps/communication/commerceFeedbackV1BetaOas3.js +5 -0
- package/build/types/sell-apps/communication/commerceMessageV1Oas3.js +5 -0
- package/build/types/sell-apps/communication/commerceNotificationV1Oas3.js +5 -0
- package/build/types/sell-apps/communication/sellNegotiationV1Oas3.js +5 -0
- package/build/types/sell-apps/listing-management/sellInventoryV1Oas3.js +5 -0
- package/build/types/sell-apps/listing-metadata/sellMetadataV1Oas3.js +5 -0
- package/build/types/sell-apps/markeitng-and-promotions/sellMarketingV1Oas3.js +5 -0
- package/build/types/sell-apps/markeitng-and-promotions/sellRecommendationV1Oas3.js +5 -0
- package/build/types/sell-apps/order-management/sellFulfillmentV1Oas3.js +5 -0
- package/build/types/sell-apps/other-apis/commerceIdentityV1Oas3.js +5 -0
- package/build/types/sell-apps/other-apis/commerceTranslationV1BetaOas3.js +5 -0
- package/build/types/sell-apps/other-apis/commerceVeroV1Oas3.js +5 -0
- package/build/types/sell-apps/other-apis/sellComplianceV1Oas3.js +5 -0
- package/build/types/sell-apps/other-apis/sellEdeliveryInternationalShippingOas3.js +5 -0
- package/build/types/sell-apps/other-apis/sellMarketingV1Oas3.js +5 -0
- package/build/types/sell-apps/other-apis/sellRecommendationV1Oas3.js +5 -0
- package/build/utils/account-management/account.js +831 -0
- package/build/utils/api-status-feed.js +83 -0
- package/build/utils/communication/feedback.js +216 -0
- package/build/utils/communication/message.js +242 -0
- package/build/utils/communication/negotiation.js +150 -0
- package/build/utils/communication/notification.js +369 -0
- package/build/utils/date-converter.js +160 -0
- package/build/utils/llm-client-detector.js +758 -0
- package/build/utils/logger.js +198 -0
- package/build/utils/oauth-helper.js +315 -0
- package/build/utils/order-management/dispute.js +369 -0
- package/build/utils/order-management/fulfillment.js +205 -0
- package/build/utils/other/compliance.js +76 -0
- package/build/utils/other/edelivery.js +241 -0
- package/build/utils/other/identity.js +13 -0
- package/build/utils/other/translation.js +41 -0
- package/build/utils/other/vero.js +90 -0
- package/build/utils/scope-helper.js +207 -0
- package/build/utils/security-checker.js +248 -0
- package/build/utils/setup-validator.js +305 -0
- package/build/utils/token-utils.js +40 -0
- package/build/utils/version.js +56 -0
- package/docs/auth/production_scopes.json +111 -0
- package/docs/auth/sandbox_scopes.json +142 -0
- package/package.json +122 -0
- package/public/icons/1024x1024.png +0 -0
- package/public/icons/128x128.png +0 -0
- package/public/icons/16x16.png +0 -0
- package/public/icons/256x256.png +0 -0
- package/public/icons/32x32.png +0 -0
- package/public/icons/48x48.png +0 -0
- 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,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
|
+
}
|