@anton.andrusenko/shopify-mcp-admin 1.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -2
- package/dist/chunk-3Y4P67GZ.js +124 -0
- package/dist/chunk-6YECVENJ.js +208 -0
- package/dist/chunk-GKGHMPEC.js +333 -0
- package/dist/index.js +5352 -2009
- package/dist/mcp-auth-UJ6MADL4.js +15 -0
- package/dist/store-MQK3GUUB.js +9 -0
- package/package.json +61 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// src/config/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var configSchema = z.object({
|
|
4
|
+
// Server operation mode (ADR-008)
|
|
5
|
+
// local: Single-tenant, credentials via env vars (default)
|
|
6
|
+
// remote: Multi-tenant, credentials via database
|
|
7
|
+
SERVER_MODE: z.enum(["local", "remote"]).default("local"),
|
|
8
|
+
// Database connection URL (required in remote mode)
|
|
9
|
+
DATABASE_URL: z.string().url().optional().describe("PostgreSQL connection URL for multi-tenant mode"),
|
|
10
|
+
// Required in local mode - store identity
|
|
11
|
+
SHOPIFY_STORE_URL: z.string().min(1, "SHOPIFY_STORE_URL is required").regex(/\.myshopify\.com$/, "Must be a valid myshopify.com domain").optional(),
|
|
12
|
+
// Authentication Option 1: Legacy Custom App (static token)
|
|
13
|
+
SHOPIFY_ACCESS_TOKEN: z.string().optional(),
|
|
14
|
+
// Authentication Option 2: Dev Dashboard (OAuth 2.0 client credentials)
|
|
15
|
+
SHOPIFY_CLIENT_ID: z.string().optional(),
|
|
16
|
+
SHOPIFY_CLIENT_SECRET: z.string().optional(),
|
|
17
|
+
// Optional with defaults
|
|
18
|
+
SHOPIFY_API_VERSION: z.string().default("2025-10"),
|
|
19
|
+
DEBUG: z.string().optional(),
|
|
20
|
+
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
21
|
+
// Log format configuration (Story 13.1: Structured Logging)
|
|
22
|
+
// json: JSON output for production (default)
|
|
23
|
+
// pretty: Human-readable output for development
|
|
24
|
+
LOG_FORMAT: z.enum(["json", "pretty"]).default("json"),
|
|
25
|
+
PORT: z.string().default("3000").transform(Number),
|
|
26
|
+
// Transport selection (AC-2.2.6, AC-2.2.7)
|
|
27
|
+
// Default: stdio for Claude Desktop compatibility
|
|
28
|
+
TRANSPORT: z.enum(["stdio", "http"]).default("stdio"),
|
|
29
|
+
// Store info cache TTL (milliseconds)
|
|
30
|
+
// Default: 5 minutes (300000ms) - configurable for performance tuning
|
|
31
|
+
STORE_INFO_CACHE_TTL_MS: z.string().optional().default("300000").transform(Number).describe("Cache TTL for store info in milliseconds (default: 5 minutes)"),
|
|
32
|
+
// Lazy loading configuration (Epic 12)
|
|
33
|
+
// Default: false - loads all tools at startup for maximum compatibility
|
|
34
|
+
// Note: Claude Desktop doesn't support dynamic tool refresh via notifications,
|
|
35
|
+
// so lazy loading is disabled by default. Set to 'true' for clients that
|
|
36
|
+
// properly handle notifications/tools/list_changed.
|
|
37
|
+
SHOPIFY_MCP_LAZY_LOADING: z.string().optional().default("false").transform((val) => val === "true" || val === "1").describe("Enable lazy loading of tools via modules (default: false)"),
|
|
38
|
+
// Role preset configuration (Story 12.3: Progressive Loading)
|
|
39
|
+
// Automatically loads appropriate modules at startup based on role
|
|
40
|
+
// Valid roles: inventory-manager, product-manager, content-manager,
|
|
41
|
+
// seo-specialist, international-manager, full-access
|
|
42
|
+
// When not set, only core module is loaded (requires manual module loading)
|
|
43
|
+
SHOPIFY_MCP_ROLE: z.string().optional().describe("Role preset for automatic module loading at startup"),
|
|
44
|
+
// AES-256-GCM encryption key for credential protection (Story 6-2)
|
|
45
|
+
// Must be exactly 64 hex characters (32 bytes = 256 bits)
|
|
46
|
+
// Required in remote mode for encrypting stored Shopify access tokens
|
|
47
|
+
// Generate with: openssl rand -hex 32
|
|
48
|
+
ENCRYPTION_KEY: z.string().regex(
|
|
49
|
+
/^[0-9a-fA-F]{64}$/,
|
|
50
|
+
"ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes). Generate with: openssl rand -hex 32"
|
|
51
|
+
).optional().describe("AES-256-GCM encryption key for credential protection (required in remote mode)"),
|
|
52
|
+
// CORS allowed origins configuration (Story 6-7)
|
|
53
|
+
// Comma-separated list of allowed origins for cross-origin requests
|
|
54
|
+
// Default: '*' (all origins allowed - suitable for development)
|
|
55
|
+
// Example: 'https://app.example.com,https://admin.example.com'
|
|
56
|
+
ALLOWED_ORIGINS: z.string().optional().describe("Comma-separated list of allowed origins for CORS"),
|
|
57
|
+
// Enable HSTS header (Story 6-7)
|
|
58
|
+
// When true, sends Strict-Transport-Security header with HTTPS responses
|
|
59
|
+
// Default: true in remote mode, false in local mode
|
|
60
|
+
ENABLE_HSTS: z.string().optional().default("false").transform((val) => val === "true" || val === "1").describe("Enable HTTP Strict Transport Security header"),
|
|
61
|
+
// Metrics endpoint configuration (Story 13-2: Metrics & Monitoring)
|
|
62
|
+
// Exposes Prometheus-format metrics at /metrics endpoint
|
|
63
|
+
// Default: true in remote mode (HTTP transport), false in local mode (STDIO)
|
|
64
|
+
METRICS_ENDPOINT_ENABLED: z.string().optional().transform((val) => val === "true" || val === "1").describe("Enable Prometheus metrics endpoint at /metrics"),
|
|
65
|
+
// Sentry error tracking DSN (Story 13.7: Production Deployment Infrastructure)
|
|
66
|
+
// Optional but recommended for production error tracking
|
|
67
|
+
// Get from: https://sentry.io → Your Project → Settings → Client Keys (DSN)
|
|
68
|
+
SENTRY_DSN: z.string().url("SENTRY_DSN must be a valid URL").optional().describe("Sentry error tracking DSN (optional but recommended for production)"),
|
|
69
|
+
// Graceful shutdown drain timeout configuration (Story 13-3: Graceful Shutdown)
|
|
70
|
+
// Time in seconds to wait for existing connections to complete before force shutdown
|
|
71
|
+
// Default: 30 seconds, max: 300 seconds (5 minutes)
|
|
72
|
+
// Railway recommends values less than termination grace period (default 10s)
|
|
73
|
+
// Kubernetes default terminationGracePeriodSeconds is 30s
|
|
74
|
+
SHUTDOWN_DRAIN_SECONDS: z.string().optional().default("30").transform(Number).pipe(
|
|
75
|
+
z.number().int("SHUTDOWN_DRAIN_SECONDS must be an integer").positive("SHUTDOWN_DRAIN_SECONDS must be positive").max(300, "SHUTDOWN_DRAIN_SECONDS cannot exceed 300 seconds (5 minutes)")
|
|
76
|
+
).describe("Graceful shutdown drain timeout in seconds (default: 30, max: 300)")
|
|
77
|
+
}).refine(
|
|
78
|
+
(data) => {
|
|
79
|
+
if (data.SERVER_MODE === "remote" && !data.DATABASE_URL) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
message: "DATABASE_URL is required when SERVER_MODE=remote. Example: postgresql://user:pass@localhost:5432/dbname",
|
|
86
|
+
path: ["DATABASE_URL"]
|
|
87
|
+
}
|
|
88
|
+
).refine(
|
|
89
|
+
(data) => {
|
|
90
|
+
if (data.SERVER_MODE === "remote" && !data.ENCRYPTION_KEY) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
message: "ENCRYPTION_KEY is required when SERVER_MODE=remote. Generate with: openssl rand -hex 32",
|
|
97
|
+
path: ["ENCRYPTION_KEY"]
|
|
98
|
+
}
|
|
99
|
+
).refine(
|
|
100
|
+
(data) => {
|
|
101
|
+
if (data.SERVER_MODE === "local" && !data.SHOPIFY_STORE_URL) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
message: "SHOPIFY_STORE_URL is required when SERVER_MODE=local",
|
|
108
|
+
path: ["SHOPIFY_STORE_URL"]
|
|
109
|
+
}
|
|
110
|
+
).refine(
|
|
111
|
+
(data) => {
|
|
112
|
+
if (data.SERVER_MODE === "remote") {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
const hasLegacyAuth = !!data.SHOPIFY_ACCESS_TOKEN;
|
|
116
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
117
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
118
|
+
const hasClientCredentials = hasClientId && hasClientSecret;
|
|
119
|
+
return hasLegacyAuth || hasClientCredentials;
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
message: "Authentication required: Provide either SHOPIFY_ACCESS_TOKEN (legacy) OR both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET (Dev Dashboard)"
|
|
123
|
+
}
|
|
124
|
+
).refine(
|
|
125
|
+
(data) => {
|
|
126
|
+
if (data.SERVER_MODE === "remote") {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
130
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
131
|
+
if (hasClientId && !hasClientSecret) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_SECRET is required when SHOPIFY_CLIENT_ID is provided"
|
|
138
|
+
}
|
|
139
|
+
).refine(
|
|
140
|
+
(data) => {
|
|
141
|
+
if (data.SERVER_MODE === "remote") {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
145
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
146
|
+
if (hasClientSecret && !hasClientId) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_ID is required when SHOPIFY_CLIENT_SECRET is provided"
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
function getAuthMode(config) {
|
|
156
|
+
if (config.SHOPIFY_ACCESS_TOKEN) {
|
|
157
|
+
return "token";
|
|
158
|
+
}
|
|
159
|
+
return "client_credentials";
|
|
160
|
+
}
|
|
161
|
+
function isDebugEnabled(debugValue) {
|
|
162
|
+
if (!debugValue) return false;
|
|
163
|
+
const normalized = debugValue.toLowerCase().trim();
|
|
164
|
+
return normalized === "1" || normalized === "true";
|
|
165
|
+
}
|
|
166
|
+
function isLazyLoadingEnabled(config) {
|
|
167
|
+
return config.SHOPIFY_MCP_LAZY_LOADING;
|
|
168
|
+
}
|
|
169
|
+
function getConfiguredRole(config) {
|
|
170
|
+
return config.SHOPIFY_MCP_ROLE;
|
|
171
|
+
}
|
|
172
|
+
function isRemoteMode(config) {
|
|
173
|
+
return config.SERVER_MODE === "remote";
|
|
174
|
+
}
|
|
175
|
+
function requireStoreUrl(config) {
|
|
176
|
+
if (!config.SHOPIFY_STORE_URL) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
"SHOPIFY_STORE_URL is not available in remote mode. Use per-request shop domain instead."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return config.SHOPIFY_STORE_URL;
|
|
182
|
+
}
|
|
183
|
+
function requireEncryptionKey(config) {
|
|
184
|
+
if (!config.ENCRYPTION_KEY) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
"ENCRYPTION_KEY is required in remote mode. Generate with: openssl rand -hex 32"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return Buffer.from(config.ENCRYPTION_KEY, "hex");
|
|
190
|
+
}
|
|
191
|
+
function isMetricsEnabled(config) {
|
|
192
|
+
if (config.METRICS_ENDPOINT_ENABLED !== void 0) {
|
|
193
|
+
return config.METRICS_ENDPOINT_ENABLED;
|
|
194
|
+
}
|
|
195
|
+
return config.SERVER_MODE === "remote" && config.TRANSPORT === "http";
|
|
196
|
+
}
|
|
197
|
+
function getShutdownDrainMs(config) {
|
|
198
|
+
return config.SHUTDOWN_DRAIN_SECONDS * 1e3;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/config/index.ts
|
|
202
|
+
var _config = null;
|
|
203
|
+
function getConfig() {
|
|
204
|
+
if (_config !== null) {
|
|
205
|
+
return _config;
|
|
206
|
+
}
|
|
207
|
+
const result = configSchema.safeParse(process.env);
|
|
208
|
+
if (!result.success) {
|
|
209
|
+
const errors = result.error.errors.map((err) => {
|
|
210
|
+
const path = err.path.join(".");
|
|
211
|
+
return ` - ${path}: ${err.message}`;
|
|
212
|
+
});
|
|
213
|
+
console.error("Configuration error:");
|
|
214
|
+
console.error(errors.join("\n"));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
_config = result.data;
|
|
218
|
+
return _config;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/utils/logger.ts
|
|
222
|
+
var SANITIZATION_PATTERNS = [
|
|
223
|
+
{ pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
224
|
+
{ pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
225
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
|
|
226
|
+
{ pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
|
|
227
|
+
{ pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" }
|
|
228
|
+
];
|
|
229
|
+
function sanitizeLogMessage(message) {
|
|
230
|
+
let result = message;
|
|
231
|
+
for (const { pattern, replacement } of SANITIZATION_PATTERNS) {
|
|
232
|
+
result = result.replace(pattern, replacement);
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
237
|
+
if (typeof obj === "string") {
|
|
238
|
+
return sanitizeLogMessage(obj);
|
|
239
|
+
}
|
|
240
|
+
if (obj === null || typeof obj !== "object") {
|
|
241
|
+
return obj;
|
|
242
|
+
}
|
|
243
|
+
if (seen.has(obj)) {
|
|
244
|
+
return "[Circular]";
|
|
245
|
+
}
|
|
246
|
+
seen.add(obj);
|
|
247
|
+
if (Array.isArray(obj)) {
|
|
248
|
+
return obj.map((item) => sanitizeObject(item, seen));
|
|
249
|
+
}
|
|
250
|
+
const result = {};
|
|
251
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
252
|
+
result[key] = sanitizeObject(value, seen);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
function safeStringify(data) {
|
|
257
|
+
try {
|
|
258
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
259
|
+
return JSON.stringify(data, (_key, value) => {
|
|
260
|
+
if (typeof value === "object" && value !== null) {
|
|
261
|
+
if (seen.has(value)) {
|
|
262
|
+
return "[Circular]";
|
|
263
|
+
}
|
|
264
|
+
seen.add(value);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
return "[Unable to stringify]";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
var log = {
|
|
273
|
+
/**
|
|
274
|
+
* Debug level logging - only outputs when DEBUG=1 or DEBUG=true
|
|
275
|
+
*
|
|
276
|
+
* @param msg - Debug message
|
|
277
|
+
* @param data - Optional data object to include (JSON-stringified)
|
|
278
|
+
*/
|
|
279
|
+
debug: (msg, data) => {
|
|
280
|
+
if (isDebugEnabled(process.env.DEBUG)) {
|
|
281
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
282
|
+
if (data) {
|
|
283
|
+
const sanitizedData = sanitizeObject(data);
|
|
284
|
+
const dataStr = safeStringify(sanitizedData);
|
|
285
|
+
console.error(`[DEBUG] ${sanitizedMsg} ${dataStr}`);
|
|
286
|
+
} else {
|
|
287
|
+
console.error(`[DEBUG] ${sanitizedMsg}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
/**
|
|
292
|
+
* Info level logging
|
|
293
|
+
*
|
|
294
|
+
* @param msg - Info message
|
|
295
|
+
*/
|
|
296
|
+
info: (msg) => {
|
|
297
|
+
console.error(`[INFO] ${sanitizeLogMessage(msg)}`);
|
|
298
|
+
},
|
|
299
|
+
/**
|
|
300
|
+
* Warning level logging
|
|
301
|
+
*
|
|
302
|
+
* @param msg - Warning message
|
|
303
|
+
*/
|
|
304
|
+
warn: (msg) => {
|
|
305
|
+
console.error(`[WARN] ${sanitizeLogMessage(msg)}`);
|
|
306
|
+
},
|
|
307
|
+
/**
|
|
308
|
+
* Error level logging
|
|
309
|
+
*
|
|
310
|
+
* @param msg - Error message
|
|
311
|
+
* @param err - Optional Error object (stack trace shown when DEBUG enabled)
|
|
312
|
+
*/
|
|
313
|
+
error: (msg, err) => {
|
|
314
|
+
console.error(`[ERROR] ${sanitizeLogMessage(msg)}`);
|
|
315
|
+
if (err && isDebugEnabled(process.env.DEBUG)) {
|
|
316
|
+
console.error(sanitizeLogMessage(err.stack || err.message));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
export {
|
|
322
|
+
getAuthMode,
|
|
323
|
+
isLazyLoadingEnabled,
|
|
324
|
+
getConfiguredRole,
|
|
325
|
+
isRemoteMode,
|
|
326
|
+
requireStoreUrl,
|
|
327
|
+
requireEncryptionKey,
|
|
328
|
+
isMetricsEnabled,
|
|
329
|
+
getShutdownDrainMs,
|
|
330
|
+
getConfig,
|
|
331
|
+
sanitizeLogMessage,
|
|
332
|
+
log
|
|
333
|
+
};
|