@anton.andrusenko/shopify-mcp-admin 2.2.1 → 2.4.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/dist/{chunk-JU5IFCVJ.js → chunk-CJXPHNYT.js} +36 -1
- package/dist/chunk-CZJ7LSEO.js +251 -0
- package/dist/{chunk-PQKNBYJN.js → chunk-H36XQ6QK.js} +6 -3
- package/dist/{chunk-LMFNHULG.js → chunk-UMNIRP6T.js} +645 -56
- package/dist/dashboard/assets/index-DVjSu1HI.js +130 -0
- package/dist/dashboard/assets/index-DVjSu1HI.js.map +1 -0
- package/dist/dashboard/assets/index-DlTP0Kre.css +1 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/mcp-icon.svg +29 -31
- package/dist/index.js +1516 -518
- package/dist/{mcp-auth-F25V6FEY.js → mcp-auth-54BVOYFJ.js} +2 -2
- package/dist/{security-44M6F2QU.js → security-6CNKRY2G.js} +4 -1
- package/dist/{store-JK2ZU6DR.js → store-5NJBYK45.js} +2 -2
- package/dist/{tools-HVUCP53D.js → tools-SVKPHJYW.js} +2 -2
- package/package.json +10 -1
- package/dist/chunk-5QMYOO4B.js +0 -146
- package/dist/dashboard/assets/index-BfNrQS4y.js +0 -120
- package/dist/dashboard/assets/index-BfNrQS4y.js.map +0 -1
- package/dist/dashboard/assets/index-HBHxyHsM.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CZJ7LSEO.js";
|
|
4
4
|
|
|
5
5
|
// src/db/client.ts
|
|
6
6
|
import { PrismaClient } from "@prisma/client";
|
|
@@ -11,10 +11,44 @@ function getPrismaClient() {
|
|
|
11
11
|
log.debug("Creating new Prisma client instance");
|
|
12
12
|
prismaInstance = new PrismaClient({
|
|
13
13
|
log: process.env.DEBUG === "true" ? ["query", "info", "warn", "error"] : ["error"]
|
|
14
|
+
// Note: Connection pool settings are configured via DATABASE_URL query params:
|
|
15
|
+
// - connection_limit: Max connections in pool (default: num_cpus * 2 + 1)
|
|
16
|
+
// - pool_timeout: Seconds to wait for available connection (default: 10)
|
|
17
|
+
// - connect_timeout: Seconds to wait for initial connection (default: 5)
|
|
18
|
+
// Example: postgresql://...?connection_limit=10&pool_timeout=30&connect_timeout=10
|
|
19
|
+
//
|
|
20
|
+
// For serverless databases (Neon, Supabase), also consider:
|
|
21
|
+
// - pgbouncer=true: Required if using PgBouncer connection pooler
|
|
14
22
|
});
|
|
15
23
|
}
|
|
16
24
|
return prismaInstance;
|
|
17
25
|
}
|
|
26
|
+
async function warmupDatabase() {
|
|
27
|
+
const prisma2 = getPrismaClient();
|
|
28
|
+
const maxRetries = 3;
|
|
29
|
+
const retryDelayMs = 1e3;
|
|
30
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
31
|
+
try {
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
await prisma2.$connect();
|
|
34
|
+
await prisma2.$queryRaw`SELECT 1`;
|
|
35
|
+
const duration = Date.now() - startTime;
|
|
36
|
+
log.info(`Database warmup completed in ${duration}ms`);
|
|
37
|
+
return;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
40
|
+
if (attempt < maxRetries) {
|
|
41
|
+
log.warn(
|
|
42
|
+
`Database warmup attempt ${attempt}/${maxRetries} failed: ${message}, retrying in ${retryDelayMs}ms...`
|
|
43
|
+
);
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
45
|
+
} else {
|
|
46
|
+
log.error(`Database warmup failed after ${maxRetries} attempts: ${message}`);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
18
52
|
async function disconnectPrisma() {
|
|
19
53
|
if (prismaInstance) {
|
|
20
54
|
log.debug("Disconnecting Prisma client");
|
|
@@ -201,6 +235,7 @@ var sessionStore = new SessionStore();
|
|
|
201
235
|
|
|
202
236
|
export {
|
|
203
237
|
getPrismaClient,
|
|
238
|
+
warmupDatabase,
|
|
204
239
|
disconnectPrisma,
|
|
205
240
|
prisma,
|
|
206
241
|
SessionStore,
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configSchema,
|
|
3
|
+
isDebugEnabled
|
|
4
|
+
} from "./chunk-EGGOXEIC.js";
|
|
5
|
+
|
|
6
|
+
// src/config/index.ts
|
|
7
|
+
var _config = null;
|
|
8
|
+
function getConfig() {
|
|
9
|
+
if (_config !== null) {
|
|
10
|
+
return _config;
|
|
11
|
+
}
|
|
12
|
+
const result = configSchema.safeParse(process.env);
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
const errors = result.error.errors.map((err) => {
|
|
15
|
+
const path = err.path.join(".");
|
|
16
|
+
return ` - ${path}: ${err.message}`;
|
|
17
|
+
});
|
|
18
|
+
console.error("Configuration error:");
|
|
19
|
+
console.error(errors.join("\n"));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
_config = result.data;
|
|
23
|
+
return _config;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/logging/sanitize.ts
|
|
27
|
+
var MAX_STRING_LENGTH = 1e3;
|
|
28
|
+
var MAX_ARRAY_ITEMS = 10;
|
|
29
|
+
var MAX_RECURSION_DEPTH = 5;
|
|
30
|
+
var MAX_OUTPUT_BYTES = 5e3;
|
|
31
|
+
var TOKEN_PATTERNS = [
|
|
32
|
+
// Shopify access tokens (shpat_xxx, shpua_xxx) - include underscores in the pattern
|
|
33
|
+
{ pattern: /shpat_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
|
|
34
|
+
{ pattern: /shpua_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
|
|
35
|
+
// Bearer tokens
|
|
36
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9_.-]+/g, replacement: "Bearer [REDACTED]" },
|
|
37
|
+
// OAuth access tokens and secrets (mcp_access_xxx, mcp_secret_xxx, mcp_client_xxx)
|
|
38
|
+
{ pattern: /mcp_access_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
|
|
39
|
+
{ pattern: /mcp_secret_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
|
|
40
|
+
{ pattern: /mcp_client_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
|
|
41
|
+
// Live/test API keys (sk_live_xxx, sk_test_xxx)
|
|
42
|
+
{ pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
|
|
43
|
+
{ pattern: /sk_test_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
|
|
44
|
+
// Generic access_token and client_secret patterns
|
|
45
|
+
{ pattern: /access_token[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "access_token=[REDACTED]" },
|
|
46
|
+
{ pattern: /client_secret[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "client_secret=[REDACTED]" }
|
|
47
|
+
];
|
|
48
|
+
function redactTokens(text) {
|
|
49
|
+
let result = text;
|
|
50
|
+
for (const { pattern, replacement } of TOKEN_PATTERNS) {
|
|
51
|
+
result = result.replace(pattern, replacement);
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function truncateString(text, maxLength = MAX_STRING_LENGTH) {
|
|
56
|
+
if (text.length <= maxLength) {
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
return `${text.substring(0, maxLength)}...[TRUNCATED]`;
|
|
60
|
+
}
|
|
61
|
+
function sanitizeParams(params, depth = 0) {
|
|
62
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
63
|
+
return "[TRUNCATED]";
|
|
64
|
+
}
|
|
65
|
+
if (params === null || params === void 0) {
|
|
66
|
+
return params;
|
|
67
|
+
}
|
|
68
|
+
if (typeof params === "string") {
|
|
69
|
+
const redacted = redactTokens(params);
|
|
70
|
+
return truncateString(redacted);
|
|
71
|
+
}
|
|
72
|
+
if (typeof params !== "object") {
|
|
73
|
+
return params;
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(params)) {
|
|
76
|
+
const limited = params.slice(0, MAX_ARRAY_ITEMS);
|
|
77
|
+
return limited.map((item) => sanitizeParams(item, depth + 1));
|
|
78
|
+
}
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const [key, value] of Object.entries(params)) {
|
|
81
|
+
result[key] = sanitizeParams(value, depth + 1);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function getByteSize(value) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.stringify(value).length;
|
|
88
|
+
} catch {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function truncateOutput(output, maxBytes = MAX_OUTPUT_BYTES) {
|
|
93
|
+
const sanitized = sanitizeParams(output);
|
|
94
|
+
const size = getByteSize(sanitized);
|
|
95
|
+
if (size <= maxBytes) {
|
|
96
|
+
return sanitized;
|
|
97
|
+
}
|
|
98
|
+
if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
|
|
99
|
+
const obj = sanitized;
|
|
100
|
+
if ("data" in obj || "errors" in obj) {
|
|
101
|
+
const result = {};
|
|
102
|
+
if (obj.errors) {
|
|
103
|
+
result.errors = obj.errors;
|
|
104
|
+
}
|
|
105
|
+
if (obj.data) {
|
|
106
|
+
result.data = "[TRUNCATED - output exceeded 5KB]";
|
|
107
|
+
}
|
|
108
|
+
if (obj.extensions && getByteSize(obj.extensions) < 200) {
|
|
109
|
+
result.extensions = obj.extensions;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
_truncated: true,
|
|
115
|
+
_message: `Output truncated: ${size} bytes exceeded ${maxBytes} byte limit`,
|
|
116
|
+
_keys: Object.keys(obj).slice(0, 10)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(sanitized)) {
|
|
120
|
+
return {
|
|
121
|
+
_truncated: true,
|
|
122
|
+
_message: `Array truncated: ${sanitized.length} items, ${size} bytes exceeded ${maxBytes} byte limit`,
|
|
123
|
+
_sample: sanitized.slice(0, 3)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return "[TRUNCATED - output exceeded 5KB]";
|
|
127
|
+
}
|
|
128
|
+
function sanitizeErrorMessage(error) {
|
|
129
|
+
if (error instanceof Error) {
|
|
130
|
+
return truncateString(redactTokens(error.message), 500);
|
|
131
|
+
}
|
|
132
|
+
if (typeof error === "string") {
|
|
133
|
+
return truncateString(redactTokens(error), 500);
|
|
134
|
+
}
|
|
135
|
+
return "Unknown error";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/utils/logger.ts
|
|
139
|
+
function sanitizeLogMessage(message) {
|
|
140
|
+
return redactTokens(message);
|
|
141
|
+
}
|
|
142
|
+
function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
143
|
+
if (typeof obj === "string") {
|
|
144
|
+
return sanitizeLogMessage(obj);
|
|
145
|
+
}
|
|
146
|
+
if (obj === null || typeof obj !== "object") {
|
|
147
|
+
return obj;
|
|
148
|
+
}
|
|
149
|
+
if (seen.has(obj)) {
|
|
150
|
+
return "[Circular]";
|
|
151
|
+
}
|
|
152
|
+
seen.add(obj);
|
|
153
|
+
if (Array.isArray(obj)) {
|
|
154
|
+
return obj.map((item) => sanitizeObject(item, seen));
|
|
155
|
+
}
|
|
156
|
+
const result = {};
|
|
157
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
158
|
+
result[key] = sanitizeObject(value, seen);
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function safeStringify(data) {
|
|
163
|
+
try {
|
|
164
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
165
|
+
return JSON.stringify(data, (_key, value) => {
|
|
166
|
+
if (typeof value === "object" && value !== null) {
|
|
167
|
+
if (seen.has(value)) {
|
|
168
|
+
return "[Circular]";
|
|
169
|
+
}
|
|
170
|
+
seen.add(value);
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
return "[Unable to stringify]";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
var log = {
|
|
179
|
+
/**
|
|
180
|
+
* Debug level logging - only outputs when DEBUG=1 or DEBUG=true
|
|
181
|
+
*
|
|
182
|
+
* @param msg - Debug message
|
|
183
|
+
* @param data - Optional data object to include (JSON-stringified)
|
|
184
|
+
*/
|
|
185
|
+
debug: (msg, data) => {
|
|
186
|
+
if (isDebugEnabled(process.env.DEBUG)) {
|
|
187
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
188
|
+
if (data) {
|
|
189
|
+
const sanitizedData = sanitizeObject(data);
|
|
190
|
+
const dataStr = safeStringify(sanitizedData);
|
|
191
|
+
console.error(`[DEBUG] ${sanitizedMsg} ${dataStr}`);
|
|
192
|
+
} else {
|
|
193
|
+
console.error(`[DEBUG] ${sanitizedMsg}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
/**
|
|
198
|
+
* Info level logging
|
|
199
|
+
*
|
|
200
|
+
* @param msg - Info message
|
|
201
|
+
* @param data - Optional data object to include (JSON-stringified)
|
|
202
|
+
*/
|
|
203
|
+
info: (msg, data) => {
|
|
204
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
205
|
+
if (data) {
|
|
206
|
+
const sanitizedData = sanitizeObject(data);
|
|
207
|
+
const dataStr = safeStringify(sanitizedData);
|
|
208
|
+
console.error(`[INFO] ${sanitizedMsg} ${dataStr}`);
|
|
209
|
+
} else {
|
|
210
|
+
console.error(`[INFO] ${sanitizedMsg}`);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
/**
|
|
214
|
+
* Warning level logging
|
|
215
|
+
*
|
|
216
|
+
* @param msg - Warning message
|
|
217
|
+
* @param data - Optional data object to include (JSON-stringified)
|
|
218
|
+
*/
|
|
219
|
+
warn: (msg, data) => {
|
|
220
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
221
|
+
if (data) {
|
|
222
|
+
const sanitizedData = sanitizeObject(data);
|
|
223
|
+
const dataStr = safeStringify(sanitizedData);
|
|
224
|
+
console.error(`[WARN] ${sanitizedMsg} ${dataStr}`);
|
|
225
|
+
} else {
|
|
226
|
+
console.error(`[WARN] ${sanitizedMsg}`);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* Error level logging
|
|
231
|
+
*
|
|
232
|
+
* @param msg - Error message
|
|
233
|
+
* @param err - Optional Error object (stack trace shown when DEBUG enabled)
|
|
234
|
+
*/
|
|
235
|
+
error: (msg, err) => {
|
|
236
|
+
console.error(`[ERROR] ${sanitizeLogMessage(msg)}`);
|
|
237
|
+
if (err && isDebugEnabled(process.env.DEBUG)) {
|
|
238
|
+
console.error(sanitizeLogMessage(err.stack || err.message));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export {
|
|
244
|
+
getConfig,
|
|
245
|
+
redactTokens,
|
|
246
|
+
sanitizeParams,
|
|
247
|
+
truncateOutput,
|
|
248
|
+
sanitizeErrorMessage,
|
|
249
|
+
sanitizeLogMessage,
|
|
250
|
+
log
|
|
251
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CZJ7LSEO.js";
|
|
4
4
|
|
|
5
5
|
// src/middleware/mcp-auth.ts
|
|
6
6
|
import { createHash } from "crypto";
|
|
@@ -170,10 +170,13 @@ async function validateOAuthAccessToken(token, prisma) {
|
|
|
170
170
|
email: accessToken.tenant.email,
|
|
171
171
|
// No apiKeyId for OAuth tokens
|
|
172
172
|
defaultShop,
|
|
173
|
-
allowedShops
|
|
173
|
+
allowedShops,
|
|
174
|
+
// OAuth client attribution (Story 16.2, AC-16.2.10)
|
|
175
|
+
oauthClientId: accessToken.client?.id,
|
|
176
|
+
oauthClientName: accessToken.client?.clientName
|
|
174
177
|
};
|
|
175
178
|
log.debug(
|
|
176
|
-
`[mcp-auth] OAuth access token validated for tenant: ${accessToken.tenantId.substring(0, 8)}
|
|
179
|
+
`[mcp-auth] OAuth access token validated for tenant: ${accessToken.tenantId.substring(0, 8)}... (client: ${accessToken.client?.clientName || "unknown"})`
|
|
177
180
|
);
|
|
178
181
|
return {
|
|
179
182
|
valid: true,
|