@anton.andrusenko/shopify-mcp-admin 2.3.0 → 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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  log
3
- } from "./chunk-5QMYOO4B.js";
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-5QMYOO4B.js";
3
+ } from "./chunk-CZJ7LSEO.js";
4
4
 
5
5
  // src/middleware/mcp-auth.ts
6
6
  import { createHash } from "crypto";
@@ -1,8 +1,12 @@
1
1
  import {
2
2
  getConfig,
3
3
  log,
4
- sanitizeLogMessage
5
- } from "./chunk-5QMYOO4B.js";
4
+ redactTokens,
5
+ sanitizeErrorMessage,
6
+ sanitizeLogMessage,
7
+ sanitizeParams,
8
+ truncateOutput
9
+ } from "./chunk-CZJ7LSEO.js";
6
10
  import {
7
11
  getAuthMode,
8
12
  getConfiguredRole,
@@ -1434,12 +1438,12 @@ function initSentry(options) {
1434
1438
  }
1435
1439
  }
1436
1440
  if (event.message) {
1437
- event.message = sanitizeErrorMessage(event.message);
1441
+ event.message = sanitizeErrorMessage2(event.message);
1438
1442
  }
1439
1443
  if (event.exception?.values) {
1440
1444
  for (const exception of event.exception.values) {
1441
1445
  if (exception.value) {
1442
- exception.value = sanitizeErrorMessage(exception.value);
1446
+ exception.value = sanitizeErrorMessage2(exception.value);
1443
1447
  }
1444
1448
  }
1445
1449
  }
@@ -1457,7 +1461,7 @@ function initSentry(options) {
1457
1461
  ]
1458
1462
  });
1459
1463
  }
1460
- function sanitizeErrorMessage(message) {
1464
+ function sanitizeErrorMessage2(message) {
1461
1465
  const patterns = [
1462
1466
  /shpat_[a-zA-Z0-9]+/g,
1463
1467
  /shpua_[a-zA-Z0-9]+/g,
@@ -1563,20 +1567,8 @@ function captureError(error, context) {
1563
1567
  }
1564
1568
 
1565
1569
  // src/logging/structured-logger.ts
1566
- var SANITIZATION_PATTERNS = [
1567
- { pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
1568
- { pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
1569
- { pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
1570
- { pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
1571
- { pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" },
1572
- { pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" }
1573
- ];
1574
1570
  function sanitizeText(text) {
1575
- let result = text;
1576
- for (const { pattern, replacement } of SANITIZATION_PATTERNS) {
1577
- result = result.replace(pattern, replacement);
1578
- }
1579
- return result;
1571
+ return redactTokens(text);
1580
1572
  }
1581
1573
  function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
1582
1574
  if (typeof obj === "string") {
@@ -1745,118 +1737,6 @@ function flushLogs() {
1745
1737
 
1746
1738
  // src/logging/tool-execution-logger.ts
1747
1739
  import { Prisma } from "@prisma/client";
1748
-
1749
- // src/logging/sanitize.ts
1750
- var MAX_STRING_LENGTH = 1e3;
1751
- var MAX_ARRAY_ITEMS = 10;
1752
- var MAX_RECURSION_DEPTH = 5;
1753
- var MAX_OUTPUT_BYTES = 5e3;
1754
- var TOKEN_PATTERNS = [
1755
- // Shopify access tokens (shpat_xxx, shpua_xxx) - include underscores in the pattern
1756
- { pattern: /shpat_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
1757
- { pattern: /shpua_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
1758
- // Bearer tokens
1759
- { pattern: /Bearer\s+[a-zA-Z0-9_.-]+/g, replacement: "Bearer [REDACTED]" },
1760
- // OAuth access tokens (mcp_access_xxx)
1761
- { pattern: /mcp_access_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1762
- // Live/test API keys (sk_live_xxx, sk_test_xxx)
1763
- { pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1764
- { pattern: /sk_test_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1765
- // Generic access_token and client_secret patterns
1766
- { pattern: /access_token[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "access_token=[REDACTED]" },
1767
- { pattern: /client_secret[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "client_secret=[REDACTED]" }
1768
- ];
1769
- function redactTokens(text) {
1770
- let result = text;
1771
- for (const { pattern, replacement } of TOKEN_PATTERNS) {
1772
- result = result.replace(pattern, replacement);
1773
- }
1774
- return result;
1775
- }
1776
- function truncateString(text, maxLength = MAX_STRING_LENGTH) {
1777
- if (text.length <= maxLength) {
1778
- return text;
1779
- }
1780
- return `${text.substring(0, maxLength)}...[TRUNCATED]`;
1781
- }
1782
- function sanitizeParams(params, depth = 0) {
1783
- if (depth > MAX_RECURSION_DEPTH) {
1784
- return "[TRUNCATED]";
1785
- }
1786
- if (params === null || params === void 0) {
1787
- return params;
1788
- }
1789
- if (typeof params === "string") {
1790
- const redacted = redactTokens(params);
1791
- return truncateString(redacted);
1792
- }
1793
- if (typeof params !== "object") {
1794
- return params;
1795
- }
1796
- if (Array.isArray(params)) {
1797
- const limited = params.slice(0, MAX_ARRAY_ITEMS);
1798
- return limited.map((item) => sanitizeParams(item, depth + 1));
1799
- }
1800
- const result = {};
1801
- for (const [key, value] of Object.entries(params)) {
1802
- result[key] = sanitizeParams(value, depth + 1);
1803
- }
1804
- return result;
1805
- }
1806
- function getByteSize(value) {
1807
- try {
1808
- return JSON.stringify(value).length;
1809
- } catch {
1810
- return 0;
1811
- }
1812
- }
1813
- function truncateOutput(output, maxBytes = MAX_OUTPUT_BYTES) {
1814
- const sanitized = sanitizeParams(output);
1815
- const size = getByteSize(sanitized);
1816
- if (size <= maxBytes) {
1817
- return sanitized;
1818
- }
1819
- if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
1820
- const obj = sanitized;
1821
- if ("data" in obj || "errors" in obj) {
1822
- const result = {};
1823
- if (obj.errors) {
1824
- result.errors = obj.errors;
1825
- }
1826
- if (obj.data) {
1827
- result.data = "[TRUNCATED - output exceeded 5KB]";
1828
- }
1829
- if (obj.extensions && getByteSize(obj.extensions) < 200) {
1830
- result.extensions = obj.extensions;
1831
- }
1832
- return result;
1833
- }
1834
- return {
1835
- _truncated: true,
1836
- _message: `Output truncated: ${size} bytes exceeded ${maxBytes} byte limit`,
1837
- _keys: Object.keys(obj).slice(0, 10)
1838
- };
1839
- }
1840
- if (Array.isArray(sanitized)) {
1841
- return {
1842
- _truncated: true,
1843
- _message: `Array truncated: ${sanitized.length} items, ${size} bytes exceeded ${maxBytes} byte limit`,
1844
- _sample: sanitized.slice(0, 3)
1845
- };
1846
- }
1847
- return "[TRUNCATED - output exceeded 5KB]";
1848
- }
1849
- function sanitizeErrorMessage2(error) {
1850
- if (error instanceof Error) {
1851
- return truncateString(redactTokens(error.message), 500);
1852
- }
1853
- if (typeof error === "string") {
1854
- return truncateString(redactTokens(error), 500);
1855
- }
1856
- return "Unknown error";
1857
- }
1858
-
1859
- // src/logging/tool-execution-logger.ts
1860
1740
  var logger = createLogger("logging/tool-execution-logger");
1861
1741
  var VALIDATION_ERROR_KEYWORDS = [
1862
1742
  "validation failed",
@@ -1960,7 +1840,7 @@ var ToolExecutionLogger = class {
1960
1840
  }
1961
1841
  const sanitizedInput = entry.inputParams ? sanitizeParams(entry.inputParams) : void 0;
1962
1842
  const sanitizedOutput = entry.output ? truncateOutput(entry.output) : void 0;
1963
- const sanitizedErrorMessage = entry.errorMessage ? sanitizeErrorMessage2(entry.errorMessage) : void 0;
1843
+ const sanitizedErrorMessage = entry.errorMessage ? sanitizeErrorMessage(entry.errorMessage) : void 0;
1964
1844
  const logData = {
1965
1845
  tenant: { connect: { id: entry.tenantId } },
1966
1846
  toolName: entry.toolName,
@@ -2077,8 +1957,7 @@ function createToolSuccess(data) {
2077
1957
  type: "text",
2078
1958
  text
2079
1959
  }
2080
- ],
2081
- structuredContent: data
1960
+ ]
2082
1961
  };
2083
1962
  }
2084
1963
 
@@ -2753,10 +2632,10 @@ function createHandlerWithContext(contextAwareHandler) {
2753
2632
  }
2754
2633
  if (isRemoteMode(config)) {
2755
2634
  log.error(
2756
- `[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This may indicate AsyncLocalStorage context was lost through MCP SDK async chain.`
2635
+ `[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This likely means no shop is connected or shop tokens need re-authentication.`
2757
2636
  );
2758
2637
  throw new Error(
2759
- "Request context not available. Please ensure you are authenticated and try again."
2638
+ "No connected Shopify store found. Please connect a store through the dashboard, or reconnect if your store was previously connected."
2760
2639
  );
2761
2640
  }
2762
2641
  const client = await getShopifyClient();
@@ -14685,6 +14564,7 @@ export {
14685
14564
  initSentry,
14686
14565
  sentryRequestMiddleware,
14687
14566
  sentryErrorHandler,
14567
+ captureError,
14688
14568
  sanitizeErrorMessage3 as sanitizeErrorMessage,
14689
14569
  createShopifyClient,
14690
14570
  getShopifyClient,
@@ -14702,6 +14582,7 @@ export {
14702
14582
  correlationMiddleware,
14703
14583
  createLogger,
14704
14584
  flushLogs,
14585
+ getToolExecutionLogger,
14705
14586
  deriveDefaultAnnotations,
14706
14587
  validateToolName,
14707
14588
  isValidToolName,