@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.
@@ -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";
@@ -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,