@lehaotech/walmart-mcp 0.5.4
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/.env.example +25 -0
- package/CHANGELOG.md +247 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/build/api/advertising/ad-client.d.ts +24 -0
- package/build/api/advertising/ad-client.js +174 -0
- package/build/api/advertising/advertising-api.d.ts +50 -0
- package/build/api/advertising/advertising-api.js +89 -0
- package/build/api/client.d.ts +19 -0
- package/build/api/client.js +150 -0
- package/build/api/feeds/feeds-api.d.ts +15 -0
- package/build/api/feeds/feeds-api.js +53 -0
- package/build/api/fulfillment/fulfillment-api.d.ts +37 -0
- package/build/api/fulfillment/fulfillment-api.js +81 -0
- package/build/api/index.d.ts +44 -0
- package/build/api/index.js +56 -0
- package/build/api/inventory/inventory-api.d.ts +27 -0
- package/build/api/inventory/inventory-api.js +49 -0
- package/build/api/items/items-api.d.ts +33 -0
- package/build/api/items/items-api.js +67 -0
- package/build/api/notifications/notifications-api.d.ts +14 -0
- package/build/api/notifications/notifications-api.js +33 -0
- package/build/api/orders/orders-api.d.ts +32 -0
- package/build/api/orders/orders-api.js +47 -0
- package/build/api/pricing/pricing-api.d.ts +32 -0
- package/build/api/pricing/pricing-api.js +60 -0
- package/build/api/reports/reports-api.d.ts +37 -0
- package/build/api/reports/reports-api.js +51 -0
- package/build/api/returns/returns-api.d.ts +26 -0
- package/build/api/returns/returns-api.js +37 -0
- package/build/api/settings/settings-api.d.ts +9 -0
- package/build/api/settings/settings-api.js +21 -0
- package/build/auth/oauth.d.ts +16 -0
- package/build/auth/oauth.js +125 -0
- package/build/config/environment.d.ts +22 -0
- package/build/config/environment.js +50 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +180 -0
- package/build/scripts/client-configs.d.ts +36 -0
- package/build/scripts/client-configs.js +132 -0
- package/build/scripts/diagnose.d.ts +15 -0
- package/build/scripts/diagnose.js +320 -0
- package/build/scripts/setup.d.ts +17 -0
- package/build/scripts/setup.js +276 -0
- package/build/tools/definitions/advertising.d.ts +664 -0
- package/build/tools/definitions/advertising.js +315 -0
- package/build/tools/definitions/discovery.d.ts +24 -0
- package/build/tools/definitions/discovery.js +65 -0
- package/build/tools/definitions/feeds.d.ts +46 -0
- package/build/tools/definitions/feeds.js +42 -0
- package/build/tools/definitions/fulfillment.d.ts +1127 -0
- package/build/tools/definitions/fulfillment.js +272 -0
- package/build/tools/definitions/inventory.d.ts +392 -0
- package/build/tools/definitions/inventory.js +182 -0
- package/build/tools/definitions/items.d.ts +447 -0
- package/build/tools/definitions/items.js +223 -0
- package/build/tools/definitions/notifications.d.ts +84 -0
- package/build/tools/definitions/notifications.js +73 -0
- package/build/tools/definitions/orders.d.ts +2659 -0
- package/build/tools/definitions/orders.js +298 -0
- package/build/tools/definitions/pricing.d.ts +724 -0
- package/build/tools/definitions/pricing.js +254 -0
- package/build/tools/definitions/reports.d.ts +223 -0
- package/build/tools/definitions/reports.js +144 -0
- package/build/tools/definitions/returns.d.ts +441 -0
- package/build/tools/definitions/returns.js +126 -0
- package/build/tools/definitions/settings.d.ts +100 -0
- package/build/tools/definitions/settings.js +52 -0
- package/build/tools/definitions/shared-schemas.d.ts +40 -0
- package/build/tools/definitions/shared-schemas.js +47 -0
- package/build/tools/definitions/token-management.d.ts +16 -0
- package/build/tools/definitions/token-management.js +41 -0
- package/build/tools/index.d.ts +6924 -0
- package/build/tools/index.js +379 -0
- package/build/utils/api-error.d.ts +41 -0
- package/build/utils/api-error.js +97 -0
- package/build/utils/endpoint-catalog.d.ts +22 -0
- package/build/utils/endpoint-catalog.js +89 -0
- package/build/utils/env-file.d.ts +12 -0
- package/build/utils/env-file.js +27 -0
- package/build/utils/known-issues.d.ts +29 -0
- package/build/utils/known-issues.js +122 -0
- package/build/utils/logger.d.ts +15 -0
- package/build/utils/logger.js +56 -0
- package/build/utils/rate-limiter.d.ts +51 -0
- package/build/utils/rate-limiter.js +109 -0
- package/package.json +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createSign, randomUUID } from 'crypto';
|
|
3
|
+
import { getAdBaseUrl } from '../../config/environment.js';
|
|
4
|
+
import { apiLogger, truncateData } from '../../utils/logger.js';
|
|
5
|
+
import { RateLimiter } from '../../utils/rate-limiter.js';
|
|
6
|
+
import { WalmartApiError, formatWalmartError } from '../../utils/api-error.js';
|
|
7
|
+
export class WalmartAdClient {
|
|
8
|
+
config;
|
|
9
|
+
http;
|
|
10
|
+
accessToken = null;
|
|
11
|
+
tokenExpiry = 0;
|
|
12
|
+
rateLimiter;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
// Advertising API: ~10 req/s, use 500 req/60s as safe window
|
|
16
|
+
this.rateLimiter = new RateLimiter(500, 60_000, 'advertising');
|
|
17
|
+
this.http = axios.create({
|
|
18
|
+
baseURL: getAdBaseUrl(config.environment),
|
|
19
|
+
timeout: 30_000,
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Accept': 'application/json',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
this.setupInterceptors();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Serialize request params into a query string deterministically (insertion
|
|
29
|
+
* order, scalars and arrays). Used to embed params into the request URL so
|
|
30
|
+
* the signed URL is byte-for-byte identical to what is actually sent —
|
|
31
|
+
* Walmart Connect validates the signature against the full request URL,
|
|
32
|
+
* including the query string.
|
|
33
|
+
*/
|
|
34
|
+
serializeParams(params) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const [key, value] of Object.entries(params)) {
|
|
37
|
+
if (value === undefined || value === null)
|
|
38
|
+
continue;
|
|
39
|
+
const values = Array.isArray(value) ? value : [value];
|
|
40
|
+
for (const v of values) {
|
|
41
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.join('&');
|
|
45
|
+
}
|
|
46
|
+
generateSignature(url, method, timestamp) {
|
|
47
|
+
if (!this.config.adPrivateKey) {
|
|
48
|
+
throw new Error('WALMART_AD_PRIVATE_KEY is required for advertising API');
|
|
49
|
+
}
|
|
50
|
+
const stringToSign = `${this.config.adConsumerId}\n${url}\n${method.toUpperCase()}\n${timestamp}\n`;
|
|
51
|
+
const sign = createSign('RSA-SHA256');
|
|
52
|
+
sign.update(stringToSign);
|
|
53
|
+
return sign.sign(this.config.adPrivateKey, 'base64');
|
|
54
|
+
}
|
|
55
|
+
async getAccessToken() {
|
|
56
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
57
|
+
return this.accessToken;
|
|
58
|
+
}
|
|
59
|
+
const tokenUrl = `${getAdBaseUrl(this.config.environment)}/v1/oauth/token`;
|
|
60
|
+
const timestamp = Date.now().toString();
|
|
61
|
+
const signature = this.generateSignature(tokenUrl, 'POST', timestamp);
|
|
62
|
+
const response = await axios.post(tokenUrl, null, {
|
|
63
|
+
headers: {
|
|
64
|
+
'WM_CONSUMER.ID': this.config.adConsumerId,
|
|
65
|
+
'WM_SEC.AUTH_SIGNATURE': signature,
|
|
66
|
+
'WM_CONSUMER.INTIMESTAMP': timestamp,
|
|
67
|
+
'WM_SEC.KEY_VERSION': this.config.adKeyVersion || '1',
|
|
68
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
|
+
},
|
|
70
|
+
params: {
|
|
71
|
+
grant_type: 'client_credentials',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
this.accessToken = response.data.access_token;
|
|
75
|
+
this.tokenExpiry = Date.now() + (response.data.expires_in || 900) * 1000 - 120_000;
|
|
76
|
+
apiLogger.info('Advertising token refreshed');
|
|
77
|
+
return this.accessToken;
|
|
78
|
+
}
|
|
79
|
+
setupInterceptors() {
|
|
80
|
+
this.http.interceptors.request.use(async (reqConfig) => {
|
|
81
|
+
if (!this.config.adConsumerId || !this.config.adPrivateKey) {
|
|
82
|
+
throw new Error('Walmart Connect credentials not configured. Set WALMART_AD_CONSUMER_ID and WALMART_AD_PRIVATE_KEY.');
|
|
83
|
+
}
|
|
84
|
+
// Pre-flight rate limit check
|
|
85
|
+
await this.rateLimiter.acquireAsync();
|
|
86
|
+
const token = await this.getAccessToken();
|
|
87
|
+
const timestamp = Date.now().toString();
|
|
88
|
+
// Embed any query params into the URL itself, then clear reqConfig.params
|
|
89
|
+
// so axios does not re-append them. This guarantees the URL we sign is
|
|
90
|
+
// identical to the URL actually sent (signature covers the query string).
|
|
91
|
+
let url = reqConfig.url || '';
|
|
92
|
+
if (reqConfig.params && typeof reqConfig.params === 'object') {
|
|
93
|
+
const qs = this.serializeParams(reqConfig.params);
|
|
94
|
+
if (qs) {
|
|
95
|
+
url += (url.includes('?') ? '&' : '?') + qs;
|
|
96
|
+
reqConfig.url = url;
|
|
97
|
+
reqConfig.params = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const fullUrl = `${reqConfig.baseURL || ''}${url}`;
|
|
101
|
+
const signature = this.generateSignature(fullUrl, reqConfig.method?.toUpperCase() || 'GET', timestamp);
|
|
102
|
+
reqConfig.headers['Authorization'] = `Bearer ${token}`;
|
|
103
|
+
reqConfig.headers['WM_CONSUMER.ID'] = this.config.adConsumerId;
|
|
104
|
+
reqConfig.headers['WM_SEC.AUTH_SIGNATURE'] = signature;
|
|
105
|
+
reqConfig.headers['WM_CONSUMER.INTIMESTAMP'] = timestamp;
|
|
106
|
+
reqConfig.headers['WM_QOS.CORRELATION_ID'] = randomUUID();
|
|
107
|
+
reqConfig.headers['WM_SEC.KEY_VERSION'] = this.config.adKeyVersion || '1';
|
|
108
|
+
apiLogger.http(`→ AD ${reqConfig.method?.toUpperCase()} ${reqConfig.url}`, {
|
|
109
|
+
params: reqConfig.params,
|
|
110
|
+
});
|
|
111
|
+
return reqConfig;
|
|
112
|
+
}, (error) => {
|
|
113
|
+
apiLogger.error('Ad request interceptor error', { error: String(error) });
|
|
114
|
+
return Promise.reject(error);
|
|
115
|
+
});
|
|
116
|
+
this.http.interceptors.response.use((response) => {
|
|
117
|
+
apiLogger.http(`← AD ${response.status} ${response.statusText}`);
|
|
118
|
+
return response;
|
|
119
|
+
}, async (error) => {
|
|
120
|
+
if (!axios.isAxiosError(error) || !error.response || !error.config) {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const status = error.response.status;
|
|
124
|
+
const config = error.config;
|
|
125
|
+
if (status === 401 && !config.__authRetried) {
|
|
126
|
+
config.__authRetried = true;
|
|
127
|
+
apiLogger.warn('AD 401 - refreshing token');
|
|
128
|
+
this.accessToken = null;
|
|
129
|
+
this.tokenExpiry = 0;
|
|
130
|
+
return this.http.request(config);
|
|
131
|
+
}
|
|
132
|
+
if (status === 429) {
|
|
133
|
+
const retryAfter = error.response.headers['retry-after'] || '60';
|
|
134
|
+
throw new Error(`Advertising rate limit exceeded. Retry after ${retryAfter}s.`);
|
|
135
|
+
}
|
|
136
|
+
if (status >= 500) {
|
|
137
|
+
const retryCount = config.__retryCount || 0;
|
|
138
|
+
if (retryCount < 3) {
|
|
139
|
+
config.__retryCount = retryCount + 1;
|
|
140
|
+
const delay = Math.pow(2, retryCount) * 1000;
|
|
141
|
+
apiLogger.warn(`AD ${status} - retry ${retryCount + 1}/3 in ${delay}ms`);
|
|
142
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
143
|
+
return this.http.request(config);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const data = error.response.data;
|
|
147
|
+
const errorMsg = data?.message
|
|
148
|
+
? `HTTP ${status}: ${data.message}`
|
|
149
|
+
: formatWalmartError(status, error.response.statusText, data);
|
|
150
|
+
apiLogger.error(`AD API Error: ${errorMsg}`, {
|
|
151
|
+
url: config.url,
|
|
152
|
+
status,
|
|
153
|
+
response: truncateData(data),
|
|
154
|
+
});
|
|
155
|
+
throw new WalmartApiError(errorMsg, status, data);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async get(endpoint, params) {
|
|
159
|
+
const response = await this.http.get(endpoint, { params });
|
|
160
|
+
return response.data;
|
|
161
|
+
}
|
|
162
|
+
async post(endpoint, data) {
|
|
163
|
+
const response = await this.http.post(endpoint, data);
|
|
164
|
+
return response.data;
|
|
165
|
+
}
|
|
166
|
+
async put(endpoint, data) {
|
|
167
|
+
const response = await this.http.put(endpoint, data);
|
|
168
|
+
return response.data;
|
|
169
|
+
}
|
|
170
|
+
async delete(endpoint, params) {
|
|
171
|
+
const response = await this.http.delete(endpoint, { params });
|
|
172
|
+
return response.data;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { WalmartAdClient } from './ad-client.js';
|
|
2
|
+
export declare class AdvertisingApi {
|
|
3
|
+
private client;
|
|
4
|
+
constructor(client: WalmartAdClient);
|
|
5
|
+
getCampaigns(params?: {
|
|
6
|
+
campaignId?: number;
|
|
7
|
+
status?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
}): Promise<unknown>;
|
|
10
|
+
createCampaign(data: object): Promise<unknown>;
|
|
11
|
+
updateCampaign(data: object): Promise<unknown>;
|
|
12
|
+
deleteCampaign(data: object): Promise<unknown>;
|
|
13
|
+
getAdGroups(params?: {
|
|
14
|
+
campaignId?: number;
|
|
15
|
+
adGroupId?: number;
|
|
16
|
+
}): Promise<unknown>;
|
|
17
|
+
createAdGroups(data: object): Promise<unknown>;
|
|
18
|
+
updateAdGroups(data: object): Promise<unknown>;
|
|
19
|
+
getAdItems(params?: {
|
|
20
|
+
campaignId?: number;
|
|
21
|
+
adGroupId?: number;
|
|
22
|
+
status?: string;
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
addAdItems(data: object): Promise<unknown>;
|
|
25
|
+
updateAdItems(data: object): Promise<unknown>;
|
|
26
|
+
getKeywords(params?: {
|
|
27
|
+
campaignId?: number;
|
|
28
|
+
adGroupId?: number;
|
|
29
|
+
keywordId?: number;
|
|
30
|
+
}): Promise<unknown>;
|
|
31
|
+
addKeywords(data: object): Promise<unknown>;
|
|
32
|
+
updateKeywords(data: object): Promise<unknown>;
|
|
33
|
+
getKeywordAnalytics(data: object): Promise<unknown>;
|
|
34
|
+
createPlacementBids(data: object): Promise<unknown>;
|
|
35
|
+
getPlacementBids(params?: {
|
|
36
|
+
campaignId?: number;
|
|
37
|
+
}): Promise<unknown>;
|
|
38
|
+
createPlatformBids(data: object): Promise<unknown>;
|
|
39
|
+
createReportSnapshot(data: object): Promise<unknown>;
|
|
40
|
+
getReportSnapshots(params?: {
|
|
41
|
+
snapshotId?: string;
|
|
42
|
+
reportType?: string;
|
|
43
|
+
}): Promise<unknown>;
|
|
44
|
+
getRealtimeStats(data: object): Promise<unknown>;
|
|
45
|
+
getLatestReportDate(): Promise<unknown>;
|
|
46
|
+
getItemRecommendations(data: object): Promise<unknown>;
|
|
47
|
+
getKeywordRecommendations(data: object): Promise<unknown>;
|
|
48
|
+
getSearchTrends(data: object): Promise<unknown>;
|
|
49
|
+
getSbaProfile(): Promise<unknown>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export class AdvertisingApi {
|
|
2
|
+
client;
|
|
3
|
+
constructor(client) {
|
|
4
|
+
this.client = client;
|
|
5
|
+
}
|
|
6
|
+
// ===== Campaigns =====
|
|
7
|
+
async getCampaigns(params) {
|
|
8
|
+
return await this.client.get('/v1/campaigns', params);
|
|
9
|
+
}
|
|
10
|
+
async createCampaign(data) {
|
|
11
|
+
return await this.client.post('/v1/campaigns', data);
|
|
12
|
+
}
|
|
13
|
+
async updateCampaign(data) {
|
|
14
|
+
return await this.client.put('/v1/campaigns', data);
|
|
15
|
+
}
|
|
16
|
+
async deleteCampaign(data) {
|
|
17
|
+
return await this.client.put('/v1/campaigns/delete', data);
|
|
18
|
+
}
|
|
19
|
+
// ===== Ad Groups =====
|
|
20
|
+
async getAdGroups(params) {
|
|
21
|
+
return await this.client.get('/v1/adGroups', params);
|
|
22
|
+
}
|
|
23
|
+
async createAdGroups(data) {
|
|
24
|
+
return await this.client.post('/v1/adGroups', data);
|
|
25
|
+
}
|
|
26
|
+
async updateAdGroups(data) {
|
|
27
|
+
return await this.client.put('/v1/adGroups', data);
|
|
28
|
+
}
|
|
29
|
+
// ===== Ad Items =====
|
|
30
|
+
async getAdItems(params) {
|
|
31
|
+
return await this.client.get('/v1/adItems', params);
|
|
32
|
+
}
|
|
33
|
+
async addAdItems(data) {
|
|
34
|
+
return await this.client.post('/v1/adItems', data);
|
|
35
|
+
}
|
|
36
|
+
async updateAdItems(data) {
|
|
37
|
+
return await this.client.put('/v1/adItems', data);
|
|
38
|
+
}
|
|
39
|
+
// ===== Keywords =====
|
|
40
|
+
async getKeywords(params) {
|
|
41
|
+
return await this.client.get('/v1/keywords', params);
|
|
42
|
+
}
|
|
43
|
+
async addKeywords(data) {
|
|
44
|
+
return await this.client.post('/v1/keywords', data);
|
|
45
|
+
}
|
|
46
|
+
async updateKeywords(data) {
|
|
47
|
+
return await this.client.put('/v1/keywords', data);
|
|
48
|
+
}
|
|
49
|
+
async getKeywordAnalytics(data) {
|
|
50
|
+
return await this.client.post('/v1/keywords/analytics', data);
|
|
51
|
+
}
|
|
52
|
+
// ===== Bid Multipliers =====
|
|
53
|
+
async createPlacementBids(data) {
|
|
54
|
+
return await this.client.post('/v1/bid-multipliers/placement', data);
|
|
55
|
+
}
|
|
56
|
+
async getPlacementBids(params) {
|
|
57
|
+
return await this.client.get('/v1/bid-multipliers/placement', params);
|
|
58
|
+
}
|
|
59
|
+
async createPlatformBids(data) {
|
|
60
|
+
return await this.client.post('/v1/bid-multipliers/platform', data);
|
|
61
|
+
}
|
|
62
|
+
// ===== Reports & Stats =====
|
|
63
|
+
async createReportSnapshot(data) {
|
|
64
|
+
return await this.client.post('/v2/snapshots/reports', data);
|
|
65
|
+
}
|
|
66
|
+
async getReportSnapshots(params) {
|
|
67
|
+
return await this.client.get('/v2/snapshots/reports', params);
|
|
68
|
+
}
|
|
69
|
+
async getRealtimeStats(data) {
|
|
70
|
+
return await this.client.post('/v1/stats', data);
|
|
71
|
+
}
|
|
72
|
+
async getLatestReportDate() {
|
|
73
|
+
return await this.client.get('/v1/latest-report-date');
|
|
74
|
+
}
|
|
75
|
+
// ===== Recommendations & Insights =====
|
|
76
|
+
async getItemRecommendations(data) {
|
|
77
|
+
return await this.client.post('/v1/recommendations/items', data);
|
|
78
|
+
}
|
|
79
|
+
async getKeywordRecommendations(data) {
|
|
80
|
+
return await this.client.post('/v1/recommendations/keywords', data);
|
|
81
|
+
}
|
|
82
|
+
async getSearchTrends(data) {
|
|
83
|
+
return await this.client.post('/v1/insights/top-search-trends', data);
|
|
84
|
+
}
|
|
85
|
+
// ===== Sponsored Brands =====
|
|
86
|
+
async getSbaProfile() {
|
|
87
|
+
return await this.client.get('/v2/sba_profile');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WalmartConfig } from '../config/environment.js';
|
|
2
|
+
import { WalmartOAuthClient } from '../auth/oauth.js';
|
|
3
|
+
import { RateLimiter } from '../utils/rate-limiter.js';
|
|
4
|
+
export declare class WalmartApiClient {
|
|
5
|
+
private config;
|
|
6
|
+
private http;
|
|
7
|
+
private authClient;
|
|
8
|
+
private rateLimiter;
|
|
9
|
+
constructor(config: WalmartConfig);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
getAuthClient(): WalmartOAuthClient;
|
|
12
|
+
/** Expose the rate limiter so the rate-budget MCP tool can snapshot it. */
|
|
13
|
+
getRateLimiter(): RateLimiter;
|
|
14
|
+
private setupInterceptors;
|
|
15
|
+
get<T = unknown>(endpoint: string, params?: object): Promise<T>;
|
|
16
|
+
post<T = unknown>(endpoint: string, data?: object, config?: object): Promise<T>;
|
|
17
|
+
put<T = unknown>(endpoint: string, data?: object): Promise<T>;
|
|
18
|
+
delete<T = unknown>(endpoint: string, params?: object): Promise<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { getBaseUrl } from '../config/environment.js';
|
|
4
|
+
import { WalmartOAuthClient } from '../auth/oauth.js';
|
|
5
|
+
import { apiLogger, truncateData } from '../utils/logger.js';
|
|
6
|
+
import { RateLimiter } from '../utils/rate-limiter.js';
|
|
7
|
+
import { WalmartApiError, formatWalmartError } from '../utils/api-error.js';
|
|
8
|
+
import { findKnownIssueHint } from '../utils/known-issues.js';
|
|
9
|
+
export class WalmartApiClient {
|
|
10
|
+
config;
|
|
11
|
+
http;
|
|
12
|
+
authClient;
|
|
13
|
+
rateLimiter;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.authClient = new WalmartOAuthClient(config);
|
|
17
|
+
// Walmart token bucket: ~20 req/s, use 1000 req/60s as safe sliding window
|
|
18
|
+
this.rateLimiter = new RateLimiter(1000, 60_000, 'marketplace');
|
|
19
|
+
this.http = axios.create({
|
|
20
|
+
baseURL: getBaseUrl(config.environment),
|
|
21
|
+
timeout: 30_000,
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Accept': 'application/json',
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
this.setupInterceptors();
|
|
28
|
+
}
|
|
29
|
+
async initialize() {
|
|
30
|
+
await this.authClient.initialize();
|
|
31
|
+
}
|
|
32
|
+
getAuthClient() {
|
|
33
|
+
return this.authClient;
|
|
34
|
+
}
|
|
35
|
+
/** Expose the rate limiter so the rate-budget MCP tool can snapshot it. */
|
|
36
|
+
getRateLimiter() {
|
|
37
|
+
return this.rateLimiter;
|
|
38
|
+
}
|
|
39
|
+
setupInterceptors() {
|
|
40
|
+
// ===== Request Interceptor =====
|
|
41
|
+
this.http.interceptors.request.use(async (reqConfig) => {
|
|
42
|
+
// 0. Pre-flight rate limit check
|
|
43
|
+
await this.rateLimiter.acquireAsync();
|
|
44
|
+
// 1. Inject access token
|
|
45
|
+
const token = await this.authClient.getAccessToken();
|
|
46
|
+
reqConfig.headers['WM_SEC.ACCESS_TOKEN'] = token;
|
|
47
|
+
// 2. Inject required Walmart headers
|
|
48
|
+
reqConfig.headers['WM_QOS.CORRELATION_ID'] = randomUUID();
|
|
49
|
+
reqConfig.headers['WM_SVC.NAME'] = this.config.svcName;
|
|
50
|
+
// 3. Optional headers
|
|
51
|
+
if (this.config.consumerChannelType) {
|
|
52
|
+
reqConfig.headers['WM_CONSUMER.CHANNEL.TYPE'] = this.config.consumerChannelType;
|
|
53
|
+
}
|
|
54
|
+
if (this.config.market !== 'us') {
|
|
55
|
+
reqConfig.headers['WM_MARKET'] = this.config.market;
|
|
56
|
+
}
|
|
57
|
+
apiLogger.http(`→ ${reqConfig.method?.toUpperCase()} ${reqConfig.url}`, {
|
|
58
|
+
params: reqConfig.params,
|
|
59
|
+
});
|
|
60
|
+
return reqConfig;
|
|
61
|
+
}, (error) => {
|
|
62
|
+
apiLogger.error('Request interceptor error', { error: String(error) });
|
|
63
|
+
return Promise.reject(error);
|
|
64
|
+
});
|
|
65
|
+
// ===== Response Interceptor =====
|
|
66
|
+
this.http.interceptors.response.use((response) => {
|
|
67
|
+
const tokenCount = response.headers['x-current-token-count'];
|
|
68
|
+
const replenish = response.headers['x-next-replenish-time'];
|
|
69
|
+
// Update rate limiter from server headers
|
|
70
|
+
this.rateLimiter.updateFromHeaders(response.headers);
|
|
71
|
+
apiLogger.http(`← ${response.status} ${response.statusText}`, tokenCount ? { rateTokens: tokenCount, replenish } : undefined);
|
|
72
|
+
return response;
|
|
73
|
+
}, async (error) => {
|
|
74
|
+
if (!axios.isAxiosError(error) || !error.response || !error.config) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const status = error.response.status;
|
|
78
|
+
const config = error.config;
|
|
79
|
+
// 401: Refresh token + retry once
|
|
80
|
+
if (status === 401 && !config.__authRetried) {
|
|
81
|
+
config.__authRetried = true;
|
|
82
|
+
apiLogger.warn('401 Unauthorized - refreshing token');
|
|
83
|
+
await this.authClient.refreshToken();
|
|
84
|
+
const newToken = await this.authClient.getAccessToken();
|
|
85
|
+
config.headers['WM_SEC.ACCESS_TOKEN'] = newToken;
|
|
86
|
+
return this.http.request(config);
|
|
87
|
+
}
|
|
88
|
+
// 429: Rate limit exceeded
|
|
89
|
+
if (status === 429) {
|
|
90
|
+
const retryAfter = error.response.headers['retry-after'] || '60';
|
|
91
|
+
apiLogger.warn(`429 Rate limit - retry after ${retryAfter}s`);
|
|
92
|
+
throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds. ` +
|
|
93
|
+
`Remaining tokens: ${error.response.headers['x-current-token-count'] || 'unknown'}`);
|
|
94
|
+
}
|
|
95
|
+
// 423: Resource locked - retry once after 60s
|
|
96
|
+
if (status === 423 && !config.__lockRetried) {
|
|
97
|
+
config.__lockRetried = true;
|
|
98
|
+
apiLogger.warn('423 Resource locked - retrying in 60s');
|
|
99
|
+
await new Promise((r) => setTimeout(r, 60_000));
|
|
100
|
+
return this.http.request(config);
|
|
101
|
+
}
|
|
102
|
+
// 5xx: Exponential backoff retry (max 3)
|
|
103
|
+
if (status >= 500) {
|
|
104
|
+
const retryCount = config.__retryCount || 0;
|
|
105
|
+
if (retryCount < 3) {
|
|
106
|
+
config.__retryCount = retryCount + 1;
|
|
107
|
+
const delay = Math.pow(2, retryCount) * 1000;
|
|
108
|
+
apiLogger.warn(`${status} Server error - retry ${retryCount + 1}/3 in ${delay}ms`);
|
|
109
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
110
|
+
return this.http.request(config);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Parse Walmart error response, preserving the full body detail.
|
|
114
|
+
const errorMsg = formatWalmartError(status, error.response.statusText, error.response.data);
|
|
115
|
+
// Build endpoint label for error context: "GET /v3/items?limit=5".
|
|
116
|
+
const method = (config.method ?? 'GET').toUpperCase();
|
|
117
|
+
const path = config.url ?? '';
|
|
118
|
+
const endpoint = `${method} ${path}`;
|
|
119
|
+
// Look up workaround hint for known-broken Walmart endpoints. Strip
|
|
120
|
+
// the query string so URL parameters do not interfere with the regex
|
|
121
|
+
// match.
|
|
122
|
+
const pathForLookup = path.split('?')[0] ?? path;
|
|
123
|
+
const hint = findKnownIssueHint(method, pathForLookup);
|
|
124
|
+
apiLogger.error(`API Error: ${errorMsg}`, {
|
|
125
|
+
url: config.url,
|
|
126
|
+
status,
|
|
127
|
+
response: truncateData(error.response.data),
|
|
128
|
+
...(hint ? { hint } : {}),
|
|
129
|
+
});
|
|
130
|
+
throw new WalmartApiError(errorMsg, status, error.response.data, endpoint, undefined, hint);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// ===== HTTP Methods =====
|
|
134
|
+
async get(endpoint, params) {
|
|
135
|
+
const response = await this.http.get(endpoint, { params });
|
|
136
|
+
return response.data;
|
|
137
|
+
}
|
|
138
|
+
async post(endpoint, data, config) {
|
|
139
|
+
const response = await this.http.post(endpoint, data, config);
|
|
140
|
+
return response.data;
|
|
141
|
+
}
|
|
142
|
+
async put(endpoint, data) {
|
|
143
|
+
const response = await this.http.put(endpoint, data);
|
|
144
|
+
return response.data;
|
|
145
|
+
}
|
|
146
|
+
async delete(endpoint, params) {
|
|
147
|
+
const response = await this.http.delete(endpoint, { params });
|
|
148
|
+
return response.data;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { WalmartApiClient } from '../client.js';
|
|
2
|
+
export declare class FeedsApi {
|
|
3
|
+
private client;
|
|
4
|
+
private basePath;
|
|
5
|
+
constructor(client: WalmartApiClient);
|
|
6
|
+
getAllFeedStatuses(params?: {
|
|
7
|
+
limit?: number;
|
|
8
|
+
offset?: number;
|
|
9
|
+
feedType?: string;
|
|
10
|
+
}): Promise<unknown>;
|
|
11
|
+
getFeedStatus(feedId: string, includeDetails?: boolean): Promise<unknown>;
|
|
12
|
+
getFeedItemStatus(feedId: string): Promise<unknown>;
|
|
13
|
+
submitFeed(feedType: string, data: object): Promise<unknown>;
|
|
14
|
+
pollFeedUntilComplete(feedId: string, maxWaitMs?: number, initialDelayMs?: number): Promise<unknown>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { feedLogger } from '../../utils/logger.js';
|
|
2
|
+
export class FeedsApi {
|
|
3
|
+
client;
|
|
4
|
+
basePath = '/v3/feeds';
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
async getAllFeedStatuses(params) {
|
|
9
|
+
return await this.client.get(this.basePath, params);
|
|
10
|
+
}
|
|
11
|
+
async getFeedStatus(feedId, includeDetails = false) {
|
|
12
|
+
if (!feedId)
|
|
13
|
+
throw new Error('feedId is required');
|
|
14
|
+
const params = includeDetails ? { includeDetails: 'true' } : undefined;
|
|
15
|
+
return await this.client.get(`${this.basePath}/${encodeURIComponent(feedId)}`, params);
|
|
16
|
+
}
|
|
17
|
+
async getFeedItemStatus(feedId) {
|
|
18
|
+
if (!feedId)
|
|
19
|
+
throw new Error('feedId is required');
|
|
20
|
+
return await this.client.get(`${this.basePath}/${encodeURIComponent(feedId)}`, {
|
|
21
|
+
includeDetails: 'true',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async submitFeed(feedType, data) {
|
|
25
|
+
if (!feedType)
|
|
26
|
+
throw new Error('feedType is required');
|
|
27
|
+
return await this.client.post(`${this.basePath}?feedType=${encodeURIComponent(feedType)}`, data, { headers: { 'Content-Type': 'application/json' } });
|
|
28
|
+
}
|
|
29
|
+
async pollFeedUntilComplete(feedId, maxWaitMs = 7_200_000, initialDelayMs = 15_000) {
|
|
30
|
+
if (!feedId)
|
|
31
|
+
throw new Error('feedId is required');
|
|
32
|
+
const intervals = [15_000, 30_000, 60_000, 120_000, 240_000];
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
let attempt = 0;
|
|
35
|
+
feedLogger.info(`Polling feed ${feedId} until complete (max ${maxWaitMs / 1000}s)`);
|
|
36
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
37
|
+
const delay = attempt < intervals.length ? intervals[attempt] : 240_000;
|
|
38
|
+
if (attempt === 0 && initialDelayMs > 0) {
|
|
39
|
+
await new Promise((r) => setTimeout(r, initialDelayMs));
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
43
|
+
}
|
|
44
|
+
const result = await this.getFeedStatus(feedId, true);
|
|
45
|
+
feedLogger.info(`Feed ${feedId} status: ${result.feedStatus} (attempt ${attempt + 1})`);
|
|
46
|
+
if (result.feedStatus === 'PROCESSED' || result.feedStatus === 'ERROR') {
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
attempt++;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Feed ${feedId} did not complete within ${maxWaitMs / 1000} seconds`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { WalmartApiClient } from '../client.js';
|
|
2
|
+
export declare class FulfillmentApi {
|
|
3
|
+
private client;
|
|
4
|
+
constructor(client: WalmartApiClient);
|
|
5
|
+
createInboundOrder(data: object): Promise<unknown>;
|
|
6
|
+
getInboundShipments(params?: {
|
|
7
|
+
limit?: number;
|
|
8
|
+
offset?: number;
|
|
9
|
+
status?: string;
|
|
10
|
+
}): Promise<unknown>;
|
|
11
|
+
getInboundErrors(params?: {
|
|
12
|
+
inboundOrderId?: string;
|
|
13
|
+
shipmentId?: string;
|
|
14
|
+
}): Promise<unknown>;
|
|
15
|
+
getShipmentItems(params?: {
|
|
16
|
+
shipmentId?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
offset?: number;
|
|
19
|
+
}): Promise<unknown>;
|
|
20
|
+
getShipmentQuantities(params?: {
|
|
21
|
+
shipmentId?: string;
|
|
22
|
+
}): Promise<unknown>;
|
|
23
|
+
getShipmentLabel(shipmentId: string): Promise<unknown>;
|
|
24
|
+
updateShipmentTracking(data: object): Promise<unknown>;
|
|
25
|
+
cancelInboundOrder(inboundOrderId: string): Promise<unknown>;
|
|
26
|
+
getShippingLabel(purchaseOrderId: string): Promise<unknown>;
|
|
27
|
+
getLabelByTracking(carrierId: string, trackingNo: string): Promise<unknown>;
|
|
28
|
+
discardLabel(data: object): Promise<unknown>;
|
|
29
|
+
getPackageTypes(carrierId: string): Promise<unknown>;
|
|
30
|
+
createMcsOrder(data: object): Promise<unknown>;
|
|
31
|
+
cancelMcsOrder(data: object): Promise<unknown>;
|
|
32
|
+
getMcsOrderStatus(orderId: string): Promise<unknown>;
|
|
33
|
+
getCarrierRateQuotes(data: object): Promise<unknown>;
|
|
34
|
+
bookCarrierShipment(data: object): Promise<unknown>;
|
|
35
|
+
getCarrierLabel(shipmentId: string): Promise<unknown>;
|
|
36
|
+
scheduleCarrierPickup(data: object): Promise<unknown>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export class FulfillmentApi {
|
|
2
|
+
client;
|
|
3
|
+
constructor(client) {
|
|
4
|
+
this.client = client;
|
|
5
|
+
}
|
|
6
|
+
// ===== WFS Inbound =====
|
|
7
|
+
async createInboundOrder(data) {
|
|
8
|
+
return await this.client.post('/v3/inbound-shipments', data);
|
|
9
|
+
}
|
|
10
|
+
async getInboundShipments(params) {
|
|
11
|
+
return await this.client.get('/v3/inbound-shipments', params);
|
|
12
|
+
}
|
|
13
|
+
async getInboundErrors(params) {
|
|
14
|
+
return await this.client.get('/v3/inbound-shipment-errors', params);
|
|
15
|
+
}
|
|
16
|
+
async getShipmentItems(params) {
|
|
17
|
+
return await this.client.get('/v3/shipment-items', params);
|
|
18
|
+
}
|
|
19
|
+
async getShipmentQuantities(params) {
|
|
20
|
+
return await this.client.get('/v3/shipment-quantities', params);
|
|
21
|
+
}
|
|
22
|
+
async getShipmentLabel(shipmentId) {
|
|
23
|
+
if (!shipmentId)
|
|
24
|
+
throw new Error('shipmentId is required');
|
|
25
|
+
return await this.client.get(`/v3/fulfillment/label/${encodeURIComponent(shipmentId)}`);
|
|
26
|
+
}
|
|
27
|
+
async updateShipmentTracking(data) {
|
|
28
|
+
return await this.client.post('/v3/shipment-tracking', data);
|
|
29
|
+
}
|
|
30
|
+
async cancelInboundOrder(inboundOrderId) {
|
|
31
|
+
if (!inboundOrderId)
|
|
32
|
+
throw new Error('inboundOrderId is required');
|
|
33
|
+
return await this.client.delete(`/v3/inbound-shipments/${encodeURIComponent(inboundOrderId)}`);
|
|
34
|
+
}
|
|
35
|
+
// ===== Shipping Labels =====
|
|
36
|
+
async getShippingLabel(purchaseOrderId) {
|
|
37
|
+
if (!purchaseOrderId)
|
|
38
|
+
throw new Error('purchaseOrderId is required');
|
|
39
|
+
return await this.client.get(`/v3/shipping/labels/${encodeURIComponent(purchaseOrderId)}`);
|
|
40
|
+
}
|
|
41
|
+
async getLabelByTracking(carrierId, trackingNo) {
|
|
42
|
+
if (!carrierId || !trackingNo)
|
|
43
|
+
throw new Error('carrierId and trackingNo are required');
|
|
44
|
+
return await this.client.get(`/v3/shipping/labels/carriers/${encodeURIComponent(carrierId)}/trackings/${encodeURIComponent(trackingNo)}`);
|
|
45
|
+
}
|
|
46
|
+
async discardLabel(data) {
|
|
47
|
+
return await this.client.post('/v3/shipping/labels/discard', data);
|
|
48
|
+
}
|
|
49
|
+
async getPackageTypes(carrierId) {
|
|
50
|
+
if (!carrierId)
|
|
51
|
+
throw new Error('carrierId is required');
|
|
52
|
+
return await this.client.get(`/v3/shipping/carriers/${encodeURIComponent(carrierId)}/packagetypes`);
|
|
53
|
+
}
|
|
54
|
+
// ===== Multichannel Solutions =====
|
|
55
|
+
async createMcsOrder(data) {
|
|
56
|
+
return await this.client.post('/v3/mcs/orders', data);
|
|
57
|
+
}
|
|
58
|
+
async cancelMcsOrder(data) {
|
|
59
|
+
return await this.client.post('/v3/mcs/orders/cancel', data);
|
|
60
|
+
}
|
|
61
|
+
async getMcsOrderStatus(orderId) {
|
|
62
|
+
if (!orderId)
|
|
63
|
+
throw new Error('orderId is required');
|
|
64
|
+
return await this.client.get(`/v3/mcs/orders/${encodeURIComponent(orderId)}`);
|
|
65
|
+
}
|
|
66
|
+
// ===== WFS Carrier =====
|
|
67
|
+
async getCarrierRateQuotes(data) {
|
|
68
|
+
return await this.client.post('/v3/wfs/carriers/quotes', data);
|
|
69
|
+
}
|
|
70
|
+
async bookCarrierShipment(data) {
|
|
71
|
+
return await this.client.post('/v3/wfs/carriers/book', data);
|
|
72
|
+
}
|
|
73
|
+
async getCarrierLabel(shipmentId) {
|
|
74
|
+
if (!shipmentId)
|
|
75
|
+
throw new Error('shipmentId is required');
|
|
76
|
+
return await this.client.get(`/v3/wfs/carriers/labels/${encodeURIComponent(shipmentId)}`);
|
|
77
|
+
}
|
|
78
|
+
async scheduleCarrierPickup(data) {
|
|
79
|
+
return await this.client.post('/v3/wfs/carriers/pickup', data);
|
|
80
|
+
}
|
|
81
|
+
}
|