@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/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ captureError,
3
4
  clearFallbackContext,
5
+ correlationMiddleware,
6
+ createLogger,
4
7
  createMultiTenantContext,
5
8
  createShopifyClient,
9
+ flushLogs,
10
+ generateCorrelationId,
6
11
  getRequestContext,
7
12
  getShopifyClient,
8
13
  getStoreAlerts,
@@ -14,28 +19,33 @@ import {
14
19
  getStorePolicies,
15
20
  getStoreShipping,
16
21
  getStoreTaxes,
22
+ getToolExecutionLogger,
23
+ initSentry,
17
24
  isMultiTenantContext,
18
25
  registerAllTools,
19
26
  requestContextStorage,
20
27
  sanitizeErrorMessage,
28
+ sentryErrorHandler,
29
+ sentryRequestMiddleware,
21
30
  setCurrentContextKey,
22
31
  setFallbackContext,
23
32
  validateShopifyToken
24
- } from "./chunk-LMFNHULG.js";
33
+ } from "./chunk-UMNIRP6T.js";
25
34
  import {
26
35
  disconnectPrisma,
27
36
  getPrismaClient,
28
37
  prisma,
29
- sessionStore
30
- } from "./chunk-JU5IFCVJ.js";
38
+ sessionStore,
39
+ warmupDatabase
40
+ } from "./chunk-CJXPHNYT.js";
31
41
  import {
32
42
  createJsonRpcError
33
- } from "./chunk-PQKNBYJN.js";
43
+ } from "./chunk-H36XQ6QK.js";
34
44
  import {
35
45
  getConfig,
36
46
  log,
37
47
  sanitizeLogMessage
38
- } from "./chunk-5QMYOO4B.js";
48
+ } from "./chunk-CZJ7LSEO.js";
39
49
  import {
40
50
  getShutdownDrainMs,
41
51
  isLazyLoadingEnabled,
@@ -49,172 +59,6 @@ import { readFileSync as readFileSync2 } from "fs";
49
59
  import { dirname as dirname2, join as join2 } from "path";
50
60
  import { fileURLToPath as fileURLToPath2 } from "url";
51
61
 
52
- // src/monitoring/sentry.ts
53
- import * as Sentry from "@sentry/node";
54
- function initSentry(options) {
55
- const { dsn, environment = "production", release } = options;
56
- if (!dsn) {
57
- return;
58
- }
59
- Sentry.init({
60
- dsn,
61
- environment,
62
- // Sample 10% of transactions for performance monitoring (AC-13.7.4)
63
- tracesSampleRate: 0.1,
64
- // Release tracking for deployment correlation
65
- release: release || "unknown",
66
- // Sanitize sensitive data before sending to Sentry
67
- beforeSend(event) {
68
- if (event.request?.headers) {
69
- const sensitiveHeaders = [
70
- "authorization",
71
- "x-api-key",
72
- "cookie",
73
- "x-shopify-access-token",
74
- "x-shopify-hmac-sha256"
75
- ];
76
- for (const header of sensitiveHeaders) {
77
- delete event.request.headers[header];
78
- }
79
- }
80
- if (event.request?.data) {
81
- const data = event.request.data;
82
- const sensitiveKeys = ["token", "password", "secret", "key", "access_token"];
83
- for (const key of Object.keys(data)) {
84
- if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
85
- data[key] = "[REDACTED]";
86
- }
87
- }
88
- }
89
- if (event.message) {
90
- event.message = sanitizeErrorMessage2(event.message);
91
- }
92
- if (event.exception?.values) {
93
- for (const exception of event.exception.values) {
94
- if (exception.value) {
95
- exception.value = sanitizeErrorMessage2(exception.value);
96
- }
97
- }
98
- }
99
- return event;
100
- },
101
- // Integrations
102
- integrations: [
103
- // Capture uncaught exceptions and unhandled rejections
104
- Sentry.onUncaughtExceptionIntegration(),
105
- Sentry.onUnhandledRejectionIntegration(),
106
- // Capture HTTP request data
107
- Sentry.requestDataIntegration(),
108
- // Deduplicate similar errors
109
- Sentry.dedupeIntegration()
110
- ]
111
- });
112
- }
113
- function sanitizeErrorMessage2(message) {
114
- const patterns = [
115
- /shpat_[a-zA-Z0-9]+/g,
116
- /shpua_[a-zA-Z0-9]+/g,
117
- /Bearer\s+[a-zA-Z0-9_-]+/g,
118
- /access_token[=:]\s*[a-zA-Z0-9_-]+/gi,
119
- /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi,
120
- /sk_live_[a-zA-Z0-9_-]+/g
121
- ];
122
- let sanitized = message;
123
- for (const pattern of patterns) {
124
- sanitized = sanitized.replace(pattern, "[REDACTED]");
125
- }
126
- return sanitized;
127
- }
128
- function sentryRequestMiddleware(req, res, next) {
129
- const context = getRequestContext();
130
- if (context) {
131
- if (context.correlationId) {
132
- Sentry.setTag("correlationId", context.correlationId);
133
- Sentry.setContext("request", {
134
- correlationId: context.correlationId
135
- });
136
- }
137
- if (isMultiTenantContext(context) && context.tenantId) {
138
- Sentry.setTag("tenantId", context.tenantId);
139
- Sentry.setContext("tenant", {
140
- tenantId: context.tenantId
141
- });
142
- }
143
- if (context.shopDomain) {
144
- Sentry.setTag("shopDomain", context.shopDomain);
145
- }
146
- }
147
- Sentry.setContext("http", {
148
- method: req.method,
149
- url: req.url,
150
- path: req.path,
151
- query: req.query,
152
- userAgent: req.get("user-agent"),
153
- ip: req.ip
154
- });
155
- Sentry.addBreadcrumb({
156
- category: "http",
157
- message: `${req.method} ${req.path}`,
158
- level: "info",
159
- data: {
160
- method: req.method,
161
- path: req.path
162
- // statusCode will be updated when response finishes (see below)
163
- }
164
- });
165
- res.on("finish", () => {
166
- Sentry.addBreadcrumb({
167
- category: "http",
168
- message: `${req.method} ${req.path} - ${res.statusCode}`,
169
- level: res.statusCode >= 400 ? "error" : "info",
170
- data: {
171
- method: req.method,
172
- path: req.path,
173
- statusCode: res.statusCode
174
- }
175
- });
176
- });
177
- next();
178
- }
179
- function sentryErrorHandler(error, req, _res, next) {
180
- Sentry.captureException(error, {
181
- tags: {
182
- path: req.path,
183
- method: req.method
184
- },
185
- extra: {
186
- url: req.url,
187
- query: req.query,
188
- body: req.body
189
- }
190
- });
191
- next(error);
192
- }
193
- function captureError(error, context) {
194
- const requestContext = getRequestContext();
195
- Sentry.withScope((scope) => {
196
- if (requestContext?.correlationId) {
197
- scope.setTag("correlationId", requestContext.correlationId);
198
- scope.setContext("request", {
199
- correlationId: requestContext.correlationId
200
- });
201
- }
202
- if (requestContext && isMultiTenantContext(requestContext) && requestContext.tenantId) {
203
- scope.setTag("tenantId", requestContext.tenantId);
204
- scope.setContext("tenant", {
205
- tenantId: requestContext.tenantId
206
- });
207
- }
208
- if (requestContext?.shopDomain) {
209
- scope.setTag("shopDomain", requestContext.shopDomain);
210
- }
211
- if (context) {
212
- scope.setContext("additional", context);
213
- }
214
- Sentry.captureException(error);
215
- });
216
- }
217
-
218
62
  // src/server.ts
219
63
  import { createRequire } from "module";
220
64
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -790,7 +634,7 @@ function createStdioTransport() {
790
634
  }
791
635
 
792
636
  // src/transports/http.ts
793
- import { randomUUID as randomUUID3 } from "crypto";
637
+ import { randomUUID as randomUUID2 } from "crypto";
794
638
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
795
639
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
796
640
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
@@ -805,194 +649,6 @@ import { z } from "zod";
805
649
 
806
650
  // src/middleware/tenant-auth.ts
807
651
  import { TenantStatus } from "@prisma/client";
808
-
809
- // src/logging/structured-logger.ts
810
- import {
811
- pino as createPino,
812
- destination as pinoDestination,
813
- transport as pinoTransport
814
- } from "pino";
815
- var SANITIZATION_PATTERNS = [
816
- { pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
817
- { pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
818
- { pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
819
- { pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
820
- { pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" },
821
- { pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" }
822
- ];
823
- function sanitizeText(text) {
824
- let result = text;
825
- for (const { pattern, replacement } of SANITIZATION_PATTERNS) {
826
- result = result.replace(pattern, replacement);
827
- }
828
- return result;
829
- }
830
- function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
831
- if (typeof obj === "string") {
832
- return sanitizeText(obj);
833
- }
834
- if (obj === null || typeof obj !== "object") {
835
- return obj;
836
- }
837
- if (seen.has(obj)) {
838
- return "[Circular]";
839
- }
840
- seen.add(obj);
841
- if (Array.isArray(obj)) {
842
- return obj.map((item) => sanitizeObject(item, seen));
843
- }
844
- const result = {};
845
- for (const [key, value] of Object.entries(obj)) {
846
- result[key] = sanitizeObject(value, seen);
847
- }
848
- return result;
849
- }
850
- function getInstanceId() {
851
- return process.env.INSTANCE_ID || process.env.HOSTNAME || "unknown";
852
- }
853
- function getLogLevel() {
854
- const level = process.env.LOG_LEVEL?.toLowerCase();
855
- if (level === "debug" || level === "info" || level === "warn" || level === "error") {
856
- return level;
857
- }
858
- return "info";
859
- }
860
- function isDevelopment() {
861
- return process.env.NODE_ENV === "development";
862
- }
863
- function createTransport() {
864
- if (isDevelopment()) {
865
- try {
866
- return {
867
- target: "pino-pretty",
868
- options: {
869
- destination: 2,
870
- // stderr (fd 2)
871
- colorize: true,
872
- translateTime: "HH:MM:ss.l",
873
- ignore: "pid,hostname"
874
- }
875
- };
876
- } catch {
877
- return void 0;
878
- }
879
- }
880
- return void 0;
881
- }
882
- function createBasePinoLogger() {
883
- const level = getLogLevel();
884
- const transport = createTransport();
885
- const instanceId = getInstanceId();
886
- const options = {
887
- level,
888
- // ISO 8601 timestamp format (AC-13.1.3)
889
- timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`,
890
- // Format level as string instead of number
891
- formatters: {
892
- level: (label) => ({ level: label })
893
- },
894
- // Base bindings (can be overridden by child loggers)
895
- // Story 13.5: Add instance ID for multi-instance deployment identification
896
- base: {
897
- instanceId
898
- // AC-13.5.1: Instance ID for load balancer distribution verification
899
- }
900
- };
901
- if (transport) {
902
- return createPino(options, pinoTransport(transport));
903
- }
904
- return createPino(options, pinoDestination({ dest: 2, sync: false }));
905
- }
906
- var basePinoLogger = createBasePinoLogger();
907
- function createLogger(module) {
908
- function getContextFields() {
909
- const context = getRequestContext();
910
- const fields = { module };
911
- if (context) {
912
- if (context.correlationId) {
913
- fields.correlationId = context.correlationId;
914
- }
915
- if (context.shopDomain) {
916
- fields.shopDomain = context.shopDomain;
917
- }
918
- if (isMultiTenantContext(context)) {
919
- fields.tenantId = context.tenantId;
920
- }
921
- }
922
- return fields;
923
- }
924
- function mergeData(data) {
925
- const contextFields = getContextFields();
926
- if (data) {
927
- const sanitizedData = sanitizeObject(data);
928
- return { ...contextFields, ...sanitizedData };
929
- }
930
- return contextFields;
931
- }
932
- function formatError(error) {
933
- return {
934
- error: {
935
- name: error.name,
936
- message: sanitizeText(error.message),
937
- stack: error.stack ? sanitizeText(error.stack) : void 0
938
- }
939
- };
940
- }
941
- const logger3 = {
942
- debug: (msg, data) => {
943
- basePinoLogger.debug(mergeData(data), sanitizeText(msg));
944
- },
945
- info: (msg, data) => {
946
- basePinoLogger.info(mergeData(data), sanitizeText(msg));
947
- },
948
- warn: (msg, data) => {
949
- basePinoLogger.warn(mergeData(data), sanitizeText(msg));
950
- },
951
- error: (msg, error, data) => {
952
- const merged = mergeData(data);
953
- if (error) {
954
- Object.assign(merged, formatError(error));
955
- captureError(error, data);
956
- }
957
- basePinoLogger.error(merged, sanitizeText(msg));
958
- },
959
- child: (bindings) => {
960
- const childModule = bindings.module || module;
961
- const childLogger = createLogger(childModule);
962
- const originalMergeData = mergeData;
963
- const enhancedMergeData = (data) => {
964
- const base = originalMergeData(data);
965
- const sanitizedBindings = sanitizeObject(bindings);
966
- return { ...base, ...sanitizedBindings };
967
- };
968
- return {
969
- debug: (msg, data) => {
970
- basePinoLogger.debug(enhancedMergeData(data), sanitizeText(msg));
971
- },
972
- info: (msg, data) => {
973
- basePinoLogger.info(enhancedMergeData(data), sanitizeText(msg));
974
- },
975
- warn: (msg, data) => {
976
- basePinoLogger.warn(enhancedMergeData(data), sanitizeText(msg));
977
- },
978
- error: (msg, error, data) => {
979
- const merged = enhancedMergeData(data);
980
- if (error) {
981
- Object.assign(merged, formatError(error));
982
- }
983
- basePinoLogger.error(merged, sanitizeText(msg));
984
- },
985
- child: childLogger.child
986
- };
987
- }
988
- };
989
- return logger3;
990
- }
991
- function flushLogs() {
992
- basePinoLogger.flush();
993
- }
994
-
995
- // src/middleware/tenant-auth.ts
996
652
  var logger = createLogger("middleware/tenant-auth");
997
653
  async function requireTenantAuth(req, res, next) {
998
654
  try {
@@ -1538,6 +1194,7 @@ var ApiKeyService = class {
1538
1194
  };
1539
1195
 
1540
1196
  // src/api/tenants.ts
1197
+ var logger2 = createLogger("api/tenants");
1541
1198
  var BCRYPT_COST_FACTOR = 12;
1542
1199
  var INITIAL_API_KEY_NAME = "Initial API Key";
1543
1200
  var registerSchema = z.object({
@@ -1654,7 +1311,7 @@ function createTenantsRouter(options) {
1654
1311
  };
1655
1312
  return res.status(201).json(response);
1656
1313
  } catch (error) {
1657
- console.error("[TENANT API] Registration error:", error);
1314
+ logger2.error("Tenant registration failed", error instanceof Error ? error : void 0);
1658
1315
  return res.status(500).json({
1659
1316
  error: "Internal Server Error",
1660
1317
  message: "An unexpected error occurred during registration"
@@ -1694,7 +1351,9 @@ function createTenantsRouter(options) {
1694
1351
  createdAt: tenant.createdAt.toISOString()
1695
1352
  });
1696
1353
  } catch (error) {
1697
- console.error("[TENANT API] Get tenant error:", error);
1354
+ logger2.error("Get tenant failed", error instanceof Error ? error : void 0, {
1355
+ tenantId: req.tenantContext?.tenantId
1356
+ });
1698
1357
  return res.status(500).json({
1699
1358
  error: "Internal Server Error",
1700
1359
  message: "An unexpected error occurred"
@@ -1758,7 +1417,9 @@ function createTenantsRouter(options) {
1758
1417
  updatedAt: updatedTenant.updatedAt.toISOString()
1759
1418
  });
1760
1419
  } catch (error) {
1761
- console.error("[TENANT API] Update tenant error:", error);
1420
+ logger2.error("Update tenant failed", error instanceof Error ? error : void 0, {
1421
+ tenantId: req.tenantContext?.tenantId
1422
+ });
1762
1423
  return res.status(500).json({
1763
1424
  error: "Internal Server Error",
1764
1425
  message: "An unexpected error occurred"
@@ -1807,7 +1468,7 @@ function createTenantsRouter(options) {
1807
1468
  where: { id: tenantId },
1808
1469
  data: { passwordHash: newPasswordHash }
1809
1470
  });
1810
- const { sessionStore: sessionStore2 } = await import("./store-JK2ZU6DR.js");
1471
+ const { sessionStore: sessionStore2 } = await import("./store-5NJBYK45.js");
1811
1472
  await sessionStore2.deleteByTenantId(tenantId);
1812
1473
  await auditLogger2.log(tenantId, {
1813
1474
  action: "tenant.password_change",
@@ -1816,7 +1477,9 @@ function createTenantsRouter(options) {
1816
1477
  });
1817
1478
  return res.status(204).send();
1818
1479
  } catch (error) {
1819
- console.error("[TENANT API] Password change error:", error);
1480
+ logger2.error("Password change failed", error instanceof Error ? error : void 0, {
1481
+ tenantId: req.tenantContext?.tenantId
1482
+ });
1820
1483
  return res.status(500).json({
1821
1484
  error: "Internal Server Error",
1822
1485
  message: "An unexpected error occurred"
@@ -1988,9 +1651,67 @@ var CredentialEncryptionService = class _CredentialEncryptionService {
1988
1651
  }
1989
1652
  return rotatedCount;
1990
1653
  }
1654
+ /**
1655
+ * Validate encryption key on startup by testing decryption of existing tokens
1656
+ *
1657
+ * This should be called during server initialization to catch ENCRYPTION_KEY
1658
+ * mismatches early, before users encounter "Request context not available" errors.
1659
+ *
1660
+ * @param prisma - Prisma client instance
1661
+ * @returns Validation result with counts of successful/failed decryptions
1662
+ *
1663
+ * @example
1664
+ * ```typescript
1665
+ * const result = await cryptoService.validateEncryptionKeyOnStartup(prisma);
1666
+ * if (result.failedCount > 0) {
1667
+ * log.error(`CRITICAL: ${result.failedCount} tokens cannot be decrypted!`);
1668
+ * // Alert Sentry, mark affected shops, etc.
1669
+ * }
1670
+ * ```
1671
+ */
1672
+ async validateEncryptionKeyOnStartup(prisma2) {
1673
+ const result = {
1674
+ totalCount: 0,
1675
+ successCount: 0,
1676
+ failedCount: 0,
1677
+ failedShopIds: []
1678
+ };
1679
+ const tenantShops = await prisma2.tenantShop.findMany({
1680
+ where: { uninstalledAt: null },
1681
+ select: { id: true, shopDomain: true, encryptedAccessToken: true }
1682
+ });
1683
+ result.totalCount = tenantShops.length;
1684
+ for (const shop of tenantShops) {
1685
+ try {
1686
+ this.decrypt(shop.encryptedAccessToken);
1687
+ result.successCount++;
1688
+ } catch {
1689
+ result.failedCount++;
1690
+ result.failedShopIds.push(shop.id);
1691
+ }
1692
+ }
1693
+ return result;
1694
+ }
1695
+ /**
1696
+ * Mark shops with decryption failures as needing re-authentication
1697
+ *
1698
+ * Called when validateEncryptionKeyOnStartup finds shops that can't be decrypted.
1699
+ * Updates their tokenStatus to 'needs_reauth' so users get clear feedback.
1700
+ *
1701
+ * @param prisma - Prisma client instance
1702
+ * @param shopIds - Array of shop IDs to mark
1703
+ */
1704
+ async markShopsAsNeedingReauth(prisma2, shopIds) {
1705
+ if (shopIds.length === 0) return;
1706
+ await prisma2.tenantShop.updateMany({
1707
+ where: { id: { in: shopIds } },
1708
+ data: { tokenStatus: "needs_reauth" }
1709
+ });
1710
+ }
1991
1711
  };
1992
1712
 
1993
1713
  // src/api/shops.ts
1714
+ var logger3 = createLogger("api/shops");
1994
1715
  var SHOP_DOMAIN_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/;
1995
1716
  var ACCESS_TOKEN_PATTERN = /^shp(at|ua)_[a-f0-9]+$/i;
1996
1717
  var manualConnectSchema = z2.object({
@@ -2106,7 +1827,9 @@ function createShopsRouter(options) {
2106
1827
  };
2107
1828
  return res.status(201).json(response);
2108
1829
  } catch (error) {
2109
- console.error("[SHOPS API] Connection error:", error);
1830
+ logger3.error("Shop connection failed", error instanceof Error ? error : void 0, {
1831
+ tenantId: req.tenantContext?.tenantId
1832
+ });
2110
1833
  return res.status(500).json({
2111
1834
  error: "Internal Server Error",
2112
1835
  message: "An unexpected error occurred during shop connection"
@@ -2165,7 +1888,9 @@ function createShopsRouter(options) {
2165
1888
  });
2166
1889
  return res.json({ shops: shopList });
2167
1890
  } catch (error) {
2168
- console.error("[SHOPS API] List error:", error);
1891
+ logger3.error("Shop listing failed", error instanceof Error ? error : void 0, {
1892
+ tenantId: req.tenantContext?.tenantId
1893
+ });
2169
1894
  return res.status(500).json({
2170
1895
  error: "Internal Server Error",
2171
1896
  message: "An unexpected error occurred while listing shops"
@@ -2218,7 +1943,10 @@ function createShopsRouter(options) {
2218
1943
  });
2219
1944
  return res.status(204).send();
2220
1945
  } catch (error) {
2221
- console.error("[SHOPS API] Disconnect error:", error);
1946
+ logger3.error("Shop disconnect failed", error instanceof Error ? error : void 0, {
1947
+ tenantId: req.tenantContext?.tenantId,
1948
+ shopId: req.params.shopId
1949
+ });
2222
1950
  return res.status(500).json({
2223
1951
  error: "Internal Server Error",
2224
1952
  message: "An unexpected error occurred while disconnecting shop"
@@ -2231,6 +1959,7 @@ function createShopsRouter(options) {
2231
1959
  // src/api/keys.ts
2232
1960
  import { Router as Router3 } from "express";
2233
1961
  import { z as z3 } from "zod";
1962
+ var logger4 = createLogger("api/keys");
2234
1963
  var DEFAULT_SCOPES = ["*"];
2235
1964
  var createKeySchema = z3.object({
2236
1965
  name: z3.string().min(1, "Name is required").max(100, "Name must be 100 characters or less"),
@@ -2284,7 +2013,9 @@ function createKeysRouter(options) {
2284
2013
  };
2285
2014
  return res.status(201).json(response);
2286
2015
  } catch (error) {
2287
- console.error("[KEYS API] Creation error:", error);
2016
+ logger4.error("API key creation failed", error instanceof Error ? error : void 0, {
2017
+ tenantId: req.tenantContext?.tenantId
2018
+ });
2288
2019
  return res.status(500).json({
2289
2020
  error: "Internal Server Error",
2290
2021
  message: "An unexpected error occurred during key creation"
@@ -2322,13 +2053,247 @@ function createKeysRouter(options) {
2322
2053
  }));
2323
2054
  return res.json({ keys: keyList });
2324
2055
  } catch (error) {
2325
- console.error("[KEYS API] List error:", error);
2056
+ logger4.error("API key listing failed", error instanceof Error ? error : void 0, {
2057
+ tenantId: req.tenantContext?.tenantId
2058
+ });
2326
2059
  return res.status(500).json({
2327
2060
  error: "Internal Server Error",
2328
2061
  message: "An unexpected error occurred while listing keys"
2329
2062
  });
2330
2063
  }
2331
2064
  });
2065
+ router.get("/metrics/batch", async (req, res) => {
2066
+ try {
2067
+ const tenantId = req.tenantContext?.tenantId;
2068
+ if (!tenantId) {
2069
+ return res.status(401).json({
2070
+ error: "Unauthorized",
2071
+ message: "Tenant context not found"
2072
+ });
2073
+ }
2074
+ const keys = await prisma2.apiKey.findMany({
2075
+ where: {
2076
+ tenantId,
2077
+ revokedAt: null
2078
+ },
2079
+ select: { id: true }
2080
+ });
2081
+ if (keys.length === 0) {
2082
+ return res.json({ metrics: {} });
2083
+ }
2084
+ let tableExists = true;
2085
+ try {
2086
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
2087
+ } catch (error) {
2088
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
2089
+ tableExists = false;
2090
+ } else {
2091
+ throw error;
2092
+ }
2093
+ }
2094
+ if (!tableExists) {
2095
+ const emptyMetrics = {};
2096
+ for (const key of keys) {
2097
+ emptyMetrics[key.id] = {
2098
+ calls7d: 0,
2099
+ calls30d: 0,
2100
+ successRate: 0,
2101
+ avgDurationMs: 0,
2102
+ lastUsedAt: null
2103
+ };
2104
+ }
2105
+ return res.json({ metrics: emptyMetrics });
2106
+ }
2107
+ const now = /* @__PURE__ */ new Date();
2108
+ const sevenDaysAgo = new Date(now);
2109
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
2110
+ const thirtyDaysAgo = new Date(now);
2111
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
2112
+ const keyIds = keys.map((k) => k.id);
2113
+ const queryStartTime = Date.now();
2114
+ const metricsRaw = await prisma2.$queryRaw`
2115
+ SELECT
2116
+ api_key_id,
2117
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo})::bigint AS calls_7d,
2118
+ COUNT(*)::bigint AS calls_30d,
2119
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo} AND status = 'SUCCESS')::bigint AS success_count_7d,
2120
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo})::bigint AS total_count_7d,
2121
+ AVG(duration_ms) FILTER (WHERE created_at >= ${sevenDaysAgo})::numeric AS avg_duration_ms,
2122
+ MAX(created_at) AS last_used_at
2123
+ FROM tool_execution_logs
2124
+ WHERE tenant_id = ${tenantId}
2125
+ AND api_key_id = ANY(${keyIds})
2126
+ AND created_at >= ${thirtyDaysAgo}
2127
+ GROUP BY api_key_id
2128
+ `;
2129
+ const metricsMap = {};
2130
+ for (const key of keys) {
2131
+ metricsMap[key.id] = {
2132
+ calls7d: 0,
2133
+ calls30d: 0,
2134
+ successRate: 0,
2135
+ avgDurationMs: 0,
2136
+ lastUsedAt: null
2137
+ };
2138
+ }
2139
+ for (const row of metricsRaw) {
2140
+ const totalCount = Number(row.total_count_7d);
2141
+ const successCount = Number(row.success_count_7d);
2142
+ const successRate = totalCount > 0 ? successCount / totalCount * 100 : 0;
2143
+ metricsMap[row.api_key_id] = {
2144
+ calls7d: Number(row.calls_7d),
2145
+ calls30d: Number(row.calls_30d),
2146
+ successRate: Math.round(successRate * 10) / 10,
2147
+ avgDurationMs: Math.round(row.avg_duration_ms ?? 0),
2148
+ lastUsedAt: row.last_used_at?.toISOString() ?? null
2149
+ };
2150
+ }
2151
+ const queryDuration = Date.now() - queryStartTime;
2152
+ if (queryDuration > 500) {
2153
+ logger4.warn("Batch API key metrics query exceeded 500ms", {
2154
+ tenantId,
2155
+ keyCount: keys.length,
2156
+ durationMs: queryDuration
2157
+ });
2158
+ }
2159
+ return res.json({ metrics: metricsMap });
2160
+ } catch (error) {
2161
+ logger4.error("batch metrics endpoint error", error, {
2162
+ correlationId: req.headers["x-correlation-id"]
2163
+ });
2164
+ return res.status(500).json({
2165
+ error: "Internal Server Error",
2166
+ message: "Failed to compute API key metrics"
2167
+ });
2168
+ }
2169
+ });
2170
+ router.get("/:keyId/metrics", async (req, res) => {
2171
+ try {
2172
+ const tenantId = req.tenantContext?.tenantId;
2173
+ if (!tenantId) {
2174
+ return res.status(401).json({
2175
+ error: "Unauthorized",
2176
+ message: "Tenant context not found"
2177
+ });
2178
+ }
2179
+ const { keyId } = req.params;
2180
+ const key = await prisma2.apiKey.findFirst({
2181
+ where: {
2182
+ id: keyId,
2183
+ tenantId,
2184
+ revokedAt: null
2185
+ },
2186
+ select: { id: true }
2187
+ });
2188
+ if (!key) {
2189
+ return res.status(404).json({
2190
+ error: "Not Found",
2191
+ message: "API key not found"
2192
+ });
2193
+ }
2194
+ let tableExists = true;
2195
+ try {
2196
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
2197
+ } catch (error) {
2198
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
2199
+ tableExists = false;
2200
+ } else {
2201
+ throw error;
2202
+ }
2203
+ }
2204
+ if (!tableExists) {
2205
+ return res.json({
2206
+ calls7d: 0,
2207
+ calls30d: 0,
2208
+ successRate: 0,
2209
+ avgDurationMs: 0,
2210
+ lastUsedAt: null
2211
+ });
2212
+ }
2213
+ const now = /* @__PURE__ */ new Date();
2214
+ const sevenDaysAgo = new Date(now);
2215
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
2216
+ const thirtyDaysAgo = new Date(now);
2217
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
2218
+ const baseWhere = {
2219
+ tenantId,
2220
+ apiKeyId: keyId
2221
+ };
2222
+ const queryStartTime = Date.now();
2223
+ const [calls7d, calls30d, totalCalls, successCount, avgDuration, lastUsed] = await Promise.all([
2224
+ // Calls in last 7 days
2225
+ prisma2.toolExecutionLog.count({
2226
+ where: {
2227
+ ...baseWhere,
2228
+ createdAt: { gte: sevenDaysAgo }
2229
+ }
2230
+ }),
2231
+ // Calls in last 30 days
2232
+ prisma2.toolExecutionLog.count({
2233
+ where: {
2234
+ ...baseWhere,
2235
+ createdAt: { gte: thirtyDaysAgo }
2236
+ }
2237
+ }),
2238
+ // Total calls (for success rate calculation)
2239
+ prisma2.toolExecutionLog.count({
2240
+ where: {
2241
+ ...baseWhere,
2242
+ createdAt: { gte: sevenDaysAgo }
2243
+ }
2244
+ }),
2245
+ // Success count (for success rate calculation)
2246
+ prisma2.toolExecutionLog.count({
2247
+ where: {
2248
+ ...baseWhere,
2249
+ createdAt: { gte: sevenDaysAgo },
2250
+ status: "SUCCESS"
2251
+ }
2252
+ }),
2253
+ // Average duration
2254
+ prisma2.toolExecutionLog.aggregate({
2255
+ where: {
2256
+ ...baseWhere,
2257
+ createdAt: { gte: sevenDaysAgo },
2258
+ durationMs: { not: null }
2259
+ },
2260
+ _avg: { durationMs: true }
2261
+ }),
2262
+ // Most recent execution
2263
+ prisma2.toolExecutionLog.findFirst({
2264
+ where: baseWhere,
2265
+ orderBy: { createdAt: "desc" },
2266
+ select: { createdAt: true }
2267
+ })
2268
+ ]);
2269
+ const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
2270
+ const queryDuration = Date.now() - queryStartTime;
2271
+ if (queryDuration > 200) {
2272
+ logger4.warn("API key metrics query exceeded 200ms", {
2273
+ keyId,
2274
+ tenantId,
2275
+ durationMs: queryDuration
2276
+ });
2277
+ }
2278
+ const response = {
2279
+ calls7d,
2280
+ calls30d,
2281
+ successRate: Math.round(successRate * 10) / 10,
2282
+ // Round to 1 decimal place
2283
+ avgDurationMs: Math.round(avgDuration._avg.durationMs ?? 0),
2284
+ lastUsedAt: lastUsed?.createdAt.toISOString() ?? null
2285
+ };
2286
+ return res.json(response);
2287
+ } catch (error) {
2288
+ logger4.error("metrics endpoint error", error, {
2289
+ correlationId: req.headers["x-correlation-id"]
2290
+ });
2291
+ return res.status(500).json({
2292
+ error: "Internal Server Error",
2293
+ message: "Failed to compute API key metrics"
2294
+ });
2295
+ }
2296
+ });
2332
2297
  router.delete("/:keyId", async (req, res) => {
2333
2298
  try {
2334
2299
  const tenantId = req.tenantContext?.tenantId;
@@ -2363,7 +2328,10 @@ function createKeysRouter(options) {
2363
2328
  });
2364
2329
  return res.status(204).send();
2365
2330
  } catch (error) {
2366
- console.error("[KEYS API] Revocation error:", error);
2331
+ logger4.error("API key revocation failed", error instanceof Error ? error : void 0, {
2332
+ tenantId: req.tenantContext?.tenantId,
2333
+ keyId: req.params.keyId
2334
+ });
2367
2335
  return res.status(500).json({
2368
2336
  error: "Internal Server Error",
2369
2337
  message: "An unexpected error occurred during key revocation"
@@ -2381,6 +2349,7 @@ import bcrypt3 from "bcrypt";
2381
2349
  import { Router as Router4 } from "express";
2382
2350
  import rateLimit2 from "express-rate-limit";
2383
2351
  import { z as z4 } from "zod";
2352
+ var logger5 = createLogger("api/auth");
2384
2353
  var loginSchema = z4.object({
2385
2354
  email: z4.string().email("Invalid email format"),
2386
2355
  password: z4.string().min(1, "Password is required")
@@ -2467,13 +2436,14 @@ function createAuthRouter(_config) {
2467
2436
  }
2468
2437
  }
2469
2438
  }
2439
+ const sameSiteSetting = isProduction ? "none" : "lax";
2470
2440
  res.cookie("session_id", sessionId, {
2471
2441
  httpOnly: true,
2472
2442
  // Prevents JavaScript access (XSS protection)
2473
2443
  secure: isProduction,
2474
- // HTTPS only in production
2475
- sameSite: "lax",
2476
- // CSRF protection
2444
+ // HTTPS only in production (required for SameSite=None)
2445
+ sameSite: sameSiteSetting,
2446
+ // 'none' in production for Claude.ai OAuth iframe compatibility
2477
2447
  path: "/",
2478
2448
  // Explicitly set path
2479
2449
  domain: cookieDomain,
@@ -2498,7 +2468,7 @@ function createAuthRouter(_config) {
2498
2468
  }
2499
2469
  });
2500
2470
  } catch (error) {
2501
- console.error("[POST /auth/login] Unexpected error:", error);
2471
+ logger5.error("Login failed", error instanceof Error ? error : void 0);
2502
2472
  res.status(500).json({
2503
2473
  error: "Internal Server Error",
2504
2474
  message: "An unexpected error occurred"
@@ -2527,7 +2497,7 @@ function createAuthRouter(_config) {
2527
2497
  res.clearCookie("session_id", {
2528
2498
  httpOnly: true,
2529
2499
  secure: isProduction,
2530
- sameSite: "lax",
2500
+ sameSite: isProduction ? "none" : "lax",
2531
2501
  path: "/",
2532
2502
  domain: cookieDomain
2533
2503
  });
@@ -2555,14 +2525,14 @@ function createAuthRouter(_config) {
2555
2525
  res.clearCookie("session_id", {
2556
2526
  httpOnly: true,
2557
2527
  secure: isProduction,
2558
- sameSite: "lax",
2528
+ sameSite: isProduction ? "none" : "lax",
2559
2529
  path: "/",
2560
2530
  domain: cookieDomain
2561
2531
  });
2562
2532
  }
2563
2533
  res.status(204).send();
2564
2534
  } catch (error) {
2565
- console.error("[POST /auth/logout] Unexpected error:", error);
2535
+ logger5.error("Logout failed", error instanceof Error ? error : void 0);
2566
2536
  res.status(500).json({
2567
2537
  error: "Internal Server Error",
2568
2538
  message: "An unexpected error occurred"
@@ -2575,9 +2545,36 @@ function createAuthRouter(_config) {
2575
2545
  // src/api/activity.ts
2576
2546
  import { Router as Router5 } from "express";
2577
2547
  import { z as z5 } from "zod";
2548
+ var logger6 = createLogger("api/activity");
2578
2549
  var usageSummaryQuerySchema = z5.object({
2579
2550
  months: z5.coerce.number().int().min(1).max(24).default(6)
2580
2551
  });
2552
+ var activityLogsQuerySchema = z5.object({
2553
+ page: z5.coerce.number().int().min(1).default(1),
2554
+ limit: z5.coerce.number().int().min(1).transform((val) => Math.min(val, 100)).default(50),
2555
+ toolName: z5.string().optional(),
2556
+ clientType: z5.enum(["api_key", "oauth_client"]).optional(),
2557
+ oauthClientId: z5.string().uuid("Invalid OAuth client ID format").optional(),
2558
+ apiKeyId: z5.string().uuid("Invalid API key ID format").optional(),
2559
+ shopId: z5.string().uuid("Invalid shop ID format").optional(),
2560
+ status: z5.enum(["SUCCESS", "ERROR", "VALIDATION_ERROR", "AUTH_ERROR", "TIMEOUT"]).optional(),
2561
+ startDate: z5.string().datetime().optional(),
2562
+ endDate: z5.string().datetime().optional()
2563
+ }).refine(
2564
+ (data) => {
2565
+ if (data.startDate && data.endDate) {
2566
+ return new Date(data.endDate) >= new Date(data.startDate);
2567
+ }
2568
+ return true;
2569
+ },
2570
+ {
2571
+ message: "endDate must be greater than or equal to startDate",
2572
+ path: ["endDate"]
2573
+ }
2574
+ );
2575
+ var activityStatsQuerySchema = z5.object({
2576
+ period: z5.enum(["7d", "30d", "90d"]).default("7d")
2577
+ });
2581
2578
  function startOfMonth(d) {
2582
2579
  const out = new Date(d);
2583
2580
  out.setDate(1);
@@ -2640,18 +2637,453 @@ function createActivityRouter() {
2640
2637
  };
2641
2638
  return res.json(response);
2642
2639
  } catch (error) {
2643
- console.error("[ACTIVITY API] usage-summary error:", error);
2640
+ logger6.error("usage-summary error", error);
2644
2641
  return res.status(500).json({
2645
2642
  error: "Internal Server Error",
2646
2643
  message: "Failed to compute usage summary"
2647
2644
  });
2648
2645
  }
2649
2646
  });
2647
+ router.get("/logs", async (req, res) => {
2648
+ try {
2649
+ const tenantId = req.tenantContext?.tenantId;
2650
+ if (!tenantId) {
2651
+ return res.status(401).json({
2652
+ error: "Unauthorized",
2653
+ message: "Tenant context not found"
2654
+ });
2655
+ }
2656
+ const parsed = activityLogsQuerySchema.safeParse(req.query);
2657
+ if (!parsed.success) {
2658
+ return res.status(400).json({
2659
+ error: "Validation Error",
2660
+ message: "Invalid query parameters",
2661
+ details: parsed.error.errors
2662
+ });
2663
+ }
2664
+ const {
2665
+ page,
2666
+ limit,
2667
+ toolName,
2668
+ clientType,
2669
+ oauthClientId,
2670
+ apiKeyId,
2671
+ shopId,
2672
+ status,
2673
+ startDate,
2674
+ endDate
2675
+ } = parsed.data;
2676
+ let tableExists = true;
2677
+ try {
2678
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
2679
+ } catch (error) {
2680
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
2681
+ tableExists = false;
2682
+ } else {
2683
+ throw error;
2684
+ }
2685
+ }
2686
+ if (!tableExists) {
2687
+ const response2 = {
2688
+ logs: [],
2689
+ pagination: {
2690
+ page,
2691
+ limit,
2692
+ total: 0,
2693
+ hasMore: false
2694
+ }
2695
+ };
2696
+ return res.json(response2);
2697
+ }
2698
+ const where = {
2699
+ tenantId,
2700
+ ...toolName && { toolName },
2701
+ ...clientType && { clientType },
2702
+ ...oauthClientId && { oauthClientId },
2703
+ ...apiKeyId && { apiKeyId },
2704
+ ...shopId && { shopId },
2705
+ ...status && { status },
2706
+ ...(startDate || endDate) && {
2707
+ createdAt: {
2708
+ ...startDate && { gte: new Date(startDate) },
2709
+ ...endDate && { lte: new Date(endDate) }
2710
+ }
2711
+ }
2712
+ };
2713
+ const skip = (page - 1) * limit;
2714
+ const take = limit;
2715
+ const [logs, total] = await Promise.all([
2716
+ prisma2.toolExecutionLog.findMany({
2717
+ where,
2718
+ skip,
2719
+ take,
2720
+ orderBy: { createdAt: "desc" },
2721
+ select: {
2722
+ id: true,
2723
+ toolName: true,
2724
+ toolModule: true,
2725
+ status: true,
2726
+ clientType: true,
2727
+ oauthClientId: true,
2728
+ apiKeyId: true,
2729
+ shopDomain: true,
2730
+ durationMs: true,
2731
+ createdAt: true
2732
+ }
2733
+ }),
2734
+ prisma2.toolExecutionLog.count({ where })
2735
+ ]);
2736
+ const clientIds = /* @__PURE__ */ new Set();
2737
+ for (const log3 of logs) {
2738
+ if (log3.oauthClientId) clientIds.add(log3.oauthClientId);
2739
+ if (log3.apiKeyId) clientIds.add(log3.apiKeyId);
2740
+ }
2741
+ const [oauthClients, apiKeys] = await Promise.all([
2742
+ clientIds.size > 0 && logs.some((log3) => log3.oauthClientId) ? prisma2.oAuthClient.findMany({
2743
+ where: {
2744
+ id: { in: Array.from(clientIds) }
2745
+ },
2746
+ select: { id: true, clientName: true }
2747
+ }) : Promise.resolve([]),
2748
+ clientIds.size > 0 && logs.some((log3) => log3.apiKeyId) ? prisma2.apiKey.findMany({
2749
+ where: {
2750
+ id: { in: Array.from(clientIds) },
2751
+ tenantId
2752
+ },
2753
+ select: { id: true, name: true }
2754
+ }) : Promise.resolve([])
2755
+ ]);
2756
+ const clientNameMap = /* @__PURE__ */ new Map();
2757
+ for (const client of oauthClients) {
2758
+ clientNameMap.set(client.id, client.clientName);
2759
+ }
2760
+ for (const key of apiKeys) {
2761
+ clientNameMap.set(key.id, key.name);
2762
+ }
2763
+ const responseLogs = logs.filter((log3) => log3.createdAt && !Number.isNaN(new Date(log3.createdAt).getTime())).map((log3) => ({
2764
+ id: log3.id,
2765
+ toolName: log3.toolName,
2766
+ toolModule: log3.toolModule ?? void 0,
2767
+ status: log3.status,
2768
+ clientType: log3.clientType,
2769
+ clientName: log3.oauthClientId ? clientNameMap.get(log3.oauthClientId) : log3.apiKeyId ? clientNameMap.get(log3.apiKeyId) : void 0,
2770
+ shopDomain: log3.shopDomain ?? void 0,
2771
+ durationMs: log3.durationMs ?? void 0,
2772
+ createdAt: new Date(log3.createdAt).toISOString()
2773
+ }));
2774
+ const response = {
2775
+ logs: responseLogs,
2776
+ pagination: {
2777
+ page,
2778
+ limit,
2779
+ total,
2780
+ hasMore: skip + take < total
2781
+ }
2782
+ };
2783
+ return res.json(response);
2784
+ } catch (error) {
2785
+ logger6.error("logs endpoint error", error, {
2786
+ correlationId: req.headers["x-correlation-id"]
2787
+ });
2788
+ return res.status(500).json({
2789
+ error: "Internal Server Error",
2790
+ message: "Failed to fetch activity logs"
2791
+ });
2792
+ }
2793
+ });
2794
+ router.get("/logs/:id", async (req, res) => {
2795
+ try {
2796
+ const tenantId = req.tenantContext?.tenantId;
2797
+ if (!tenantId) {
2798
+ return res.status(401).json({
2799
+ error: "Unauthorized",
2800
+ message: "Tenant context not found"
2801
+ });
2802
+ }
2803
+ const logId = req.params.id;
2804
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2805
+ if (!uuidRegex.test(logId)) {
2806
+ return res.status(400).json({
2807
+ error: "Validation Error",
2808
+ message: "Invalid log ID format"
2809
+ });
2810
+ }
2811
+ let tableExists = true;
2812
+ try {
2813
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
2814
+ } catch {
2815
+ tableExists = false;
2816
+ }
2817
+ if (!tableExists) {
2818
+ return res.status(404).json({
2819
+ error: "Not Found",
2820
+ message: "Log entry not found"
2821
+ });
2822
+ }
2823
+ const log3 = await prisma2.toolExecutionLog.findFirst({
2824
+ where: {
2825
+ id: logId,
2826
+ tenantId
2827
+ // Ensure tenant isolation
2828
+ },
2829
+ select: {
2830
+ id: true,
2831
+ toolName: true,
2832
+ toolModule: true,
2833
+ status: true,
2834
+ clientType: true,
2835
+ oauthClientId: true,
2836
+ apiKeyId: true,
2837
+ shopDomain: true,
2838
+ durationMs: true,
2839
+ createdAt: true,
2840
+ inputParams: true,
2841
+ outputSummary: true,
2842
+ correlationId: true,
2843
+ errorCode: true,
2844
+ errorMessage: true
2845
+ }
2846
+ });
2847
+ if (!log3) {
2848
+ return res.status(404).json({
2849
+ error: "Not Found",
2850
+ message: "Log entry not found"
2851
+ });
2852
+ }
2853
+ let clientName;
2854
+ if (log3.oauthClientId) {
2855
+ const oauthClient = await prisma2.oAuthClient.findUnique({
2856
+ where: { id: log3.oauthClientId },
2857
+ select: { clientName: true }
2858
+ });
2859
+ clientName = oauthClient?.clientName;
2860
+ } else if (log3.apiKeyId) {
2861
+ const apiKey = await prisma2.apiKey.findUnique({
2862
+ where: { id: log3.apiKeyId, tenantId },
2863
+ select: { name: true }
2864
+ });
2865
+ clientName = apiKey?.name;
2866
+ }
2867
+ const response = {
2868
+ id: log3.id,
2869
+ toolName: log3.toolName,
2870
+ toolModule: log3.toolModule ?? void 0,
2871
+ status: log3.status,
2872
+ clientType: log3.clientType,
2873
+ clientName,
2874
+ shopDomain: log3.shopDomain ?? void 0,
2875
+ durationMs: log3.durationMs ?? void 0,
2876
+ createdAt: new Date(log3.createdAt).toISOString(),
2877
+ inputParams: log3.inputParams ?? void 0,
2878
+ outputSummary: log3.outputSummary ?? void 0,
2879
+ correlationId: log3.correlationId ?? void 0,
2880
+ errorCode: log3.errorCode ?? void 0,
2881
+ errorMessage: log3.errorMessage ?? void 0
2882
+ };
2883
+ return res.json(response);
2884
+ } catch (error) {
2885
+ logger6.error("log details endpoint error", error, {
2886
+ correlationId: req.headers["x-correlation-id"],
2887
+ logId: req.params.id
2888
+ });
2889
+ return res.status(500).json({
2890
+ error: "Internal Server Error",
2891
+ message: "Failed to fetch log details"
2892
+ });
2893
+ }
2894
+ });
2895
+ router.get("/stats", async (req, res) => {
2896
+ try {
2897
+ const tenantId = req.tenantContext?.tenantId;
2898
+ if (!tenantId) {
2899
+ return res.status(401).json({
2900
+ error: "Unauthorized",
2901
+ message: "Tenant context not found"
2902
+ });
2903
+ }
2904
+ const parsed = activityStatsQuerySchema.safeParse(req.query);
2905
+ if (!parsed.success) {
2906
+ return res.status(400).json({
2907
+ error: "Validation Error",
2908
+ message: "Invalid query parameters",
2909
+ details: parsed.error.errors
2910
+ });
2911
+ }
2912
+ const { period } = parsed.data;
2913
+ const now = /* @__PURE__ */ new Date();
2914
+ const days = period === "7d" ? 7 : period === "30d" ? 30 : 90;
2915
+ const startDate = new Date(now);
2916
+ startDate.setDate(startDate.getDate() - days);
2917
+ const baseWhere = {
2918
+ tenantId,
2919
+ createdAt: { gte: startDate }
2920
+ };
2921
+ let tableExists = true;
2922
+ try {
2923
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
2924
+ } catch (error) {
2925
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
2926
+ tableExists = false;
2927
+ } else {
2928
+ throw error;
2929
+ }
2930
+ }
2931
+ if (!tableExists) {
2932
+ const response2 = {
2933
+ period,
2934
+ totalCalls: 0,
2935
+ successRate: 0,
2936
+ avgDurationMs: 0,
2937
+ topTools: [],
2938
+ byClient: [],
2939
+ byShop: [],
2940
+ byDay: []
2941
+ };
2942
+ return res.json(response2);
2943
+ }
2944
+ const [totalCalls, successCount, avgDuration, topTools, byClientGroup, byShopRaw] = await Promise.all([
2945
+ // Total calls
2946
+ prisma2.toolExecutionLog.count({ where: baseWhere }),
2947
+ // Success count
2948
+ prisma2.toolExecutionLog.count({
2949
+ where: {
2950
+ ...baseWhere,
2951
+ status: "SUCCESS"
2952
+ }
2953
+ }),
2954
+ // Average duration
2955
+ prisma2.toolExecutionLog.aggregate({
2956
+ where: baseWhere,
2957
+ _avg: { durationMs: true }
2958
+ }),
2959
+ // Top 10 tools
2960
+ prisma2.toolExecutionLog.groupBy({
2961
+ by: ["toolName"],
2962
+ where: baseWhere,
2963
+ _count: { id: true },
2964
+ orderBy: { _count: { id: "desc" } },
2965
+ take: 10
2966
+ }),
2967
+ // By client
2968
+ prisma2.toolExecutionLog.groupBy({
2969
+ by: ["clientType", "oauthClientId", "apiKeyId"],
2970
+ where: baseWhere,
2971
+ _count: { id: true }
2972
+ }),
2973
+ // By shop - using raw SQL for better aggregation
2974
+ tableExists ? prisma2.$queryRaw`
2975
+ SELECT
2976
+ shop_domain AS "shopDomain",
2977
+ COUNT(*)::bigint AS calls,
2978
+ COUNT(*) FILTER (WHERE status = 'SUCCESS')::bigint AS "successCount",
2979
+ AVG(duration_ms)::numeric AS "avgDuration"
2980
+ FROM tool_execution_logs
2981
+ WHERE tenant_id = ${tenantId}
2982
+ AND created_at >= ${startDate}
2983
+ AND shop_domain IS NOT NULL
2984
+ GROUP BY shop_domain
2985
+ ORDER BY calls DESC
2986
+ ` : Promise.resolve([])
2987
+ ]);
2988
+ const byDayRaw = tableExists ? await prisma2.$queryRaw`
2989
+ SELECT
2990
+ date_trunc('day', created_at)::date AS date,
2991
+ COUNT(*)::bigint AS calls,
2992
+ COUNT(*) FILTER (WHERE status != 'SUCCESS')::bigint AS errors
2993
+ FROM tool_execution_logs
2994
+ WHERE tenant_id = ${tenantId}
2995
+ AND created_at >= ${startDate}
2996
+ GROUP BY 1
2997
+ ORDER BY 1 ASC
2998
+ ` : [];
2999
+ const clientIds = /* @__PURE__ */ new Set();
3000
+ for (const group of byClientGroup) {
3001
+ if (group.oauthClientId) clientIds.add(group.oauthClientId);
3002
+ if (group.apiKeyId) clientIds.add(group.apiKeyId);
3003
+ }
3004
+ const [oauthClients, apiKeys] = await Promise.all([
3005
+ clientIds.size > 0 && byClientGroup.some((g) => g.oauthClientId) ? prisma2.oAuthClient.findMany({
3006
+ where: {
3007
+ id: { in: Array.from(clientIds) }
3008
+ },
3009
+ select: { id: true, clientName: true }
3010
+ }) : Promise.resolve([]),
3011
+ clientIds.size > 0 && byClientGroup.some((g) => g.apiKeyId) ? prisma2.apiKey.findMany({
3012
+ where: {
3013
+ id: { in: Array.from(clientIds) },
3014
+ tenantId
3015
+ },
3016
+ select: { id: true, name: true }
3017
+ }) : Promise.resolve([])
3018
+ ]);
3019
+ const clientNameMap = /* @__PURE__ */ new Map();
3020
+ for (const client of oauthClients) {
3021
+ clientNameMap.set(client.id, client.clientName);
3022
+ }
3023
+ for (const key of apiKeys) {
3024
+ clientNameMap.set(key.id, key.name);
3025
+ }
3026
+ const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
3027
+ const avgDurationMs = avgDuration._avg.durationMs ?? 0;
3028
+ const topToolsFormatted = topTools.map((tool) => ({
3029
+ name: tool.toolName,
3030
+ calls: tool._count.id
3031
+ }));
3032
+ const byClientFormatted = byClientGroup.map((group) => ({
3033
+ clientId: group.oauthClientId ?? void 0,
3034
+ clientName: group.oauthClientId ? clientNameMap.get(group.oauthClientId) : void 0,
3035
+ apiKeyId: group.apiKeyId ?? void 0,
3036
+ keyName: group.apiKeyId ? clientNameMap.get(group.apiKeyId) : void 0,
3037
+ type: group.clientType === "oauth_client" ? "oauth" : "api_key",
3038
+ calls: group._count.id
3039
+ }));
3040
+ const byShopFormatted = byShopRaw.filter((row) => row.shopDomain).map((row) => {
3041
+ const calls = Number(row.calls);
3042
+ const successCount2 = Number(row.successCount ?? 0);
3043
+ const successRate2 = calls > 0 ? successCount2 / calls * 100 : 0;
3044
+ const avgDurationMs2 = row.avgDuration ? Number(row.avgDuration) : null;
3045
+ return {
3046
+ shopDomain: row.shopDomain,
3047
+ calls,
3048
+ successRate: Math.round(successRate2 * 100) / 100,
3049
+ // Round to 2 decimal places
3050
+ avgDurationMs: avgDurationMs2 ? Math.round(avgDurationMs2) : void 0
3051
+ };
3052
+ });
3053
+ const byDayFormatted = byDayRaw.filter((row) => row.date && !Number.isNaN(new Date(row.date).getTime())).map((row) => ({
3054
+ date: new Date(row.date).toISOString().split("T")[0],
3055
+ // YYYY-MM-DD
3056
+ calls: Number(row.calls),
3057
+ errors: Number(row.errors)
3058
+ }));
3059
+ const response = {
3060
+ period,
3061
+ totalCalls,
3062
+ successRate: Math.round(successRate * 100) / 100,
3063
+ // Round to 2 decimal places
3064
+ avgDurationMs: Math.round(avgDurationMs),
3065
+ topTools: topToolsFormatted,
3066
+ byClient: byClientFormatted,
3067
+ byShop: byShopFormatted,
3068
+ byDay: byDayFormatted
3069
+ };
3070
+ return res.json(response);
3071
+ } catch (error) {
3072
+ logger6.error("stats endpoint error", error, {
3073
+ correlationId: req.headers["x-correlation-id"]
3074
+ });
3075
+ return res.status(500).json({
3076
+ error: "Internal Server Error",
3077
+ message: "Failed to compute activity statistics"
3078
+ });
3079
+ }
3080
+ });
2650
3081
  return router;
2651
3082
  }
2652
3083
 
2653
3084
  // src/api/oauth-clients.ts
2654
3085
  import { Router as Router6 } from "express";
3086
+ var logger7 = createLogger("api/oauth-clients");
2655
3087
  var OAUTH_CLIENT_REVOKE_ACTION = "oauth_client:revoke";
2656
3088
  function createOAuthClientsRouter(options) {
2657
3089
  const { auditLogger: auditLogger2 } = options;
@@ -2669,10 +3101,309 @@ function createOAuthClientsRouter(options) {
2669
3101
  const clients = await getAuthorizedClients(prisma2, tenantId);
2670
3102
  return res.json({ clients });
2671
3103
  } catch (error) {
2672
- console.error("[OAUTH-CLIENTS API] List error:", error);
3104
+ logger7.error("OAuth clients listing failed", error instanceof Error ? error : void 0, {
3105
+ tenantId: req.tenantContext?.tenantId
3106
+ });
3107
+ return res.status(500).json({
3108
+ error: "Internal Server Error",
3109
+ message: "An unexpected error occurred while listing OAuth clients"
3110
+ });
3111
+ }
3112
+ });
3113
+ router.get("/metrics/batch", async (req, res) => {
3114
+ try {
3115
+ const tenantId = req.tenantContext?.tenantId;
3116
+ if (!tenantId) {
3117
+ return res.status(401).json({
3118
+ error: "Unauthorized",
3119
+ message: "Tenant context not found"
3120
+ });
3121
+ }
3122
+ const activeTokens = await prisma2.oAuthRefreshToken.findMany({
3123
+ where: {
3124
+ tenantId,
3125
+ revokedAt: null
3126
+ },
3127
+ select: {
3128
+ clientId: true,
3129
+ // This is the external clientId string
3130
+ client: {
3131
+ select: { id: true }
3132
+ // This is the database CUID
3133
+ }
3134
+ },
3135
+ distinct: ["clientId"]
3136
+ });
3137
+ if (activeTokens.length === 0) {
3138
+ return res.json({ metrics: {} });
3139
+ }
3140
+ let tableExists = true;
3141
+ try {
3142
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
3143
+ } catch (error) {
3144
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
3145
+ tableExists = false;
3146
+ } else {
3147
+ throw error;
3148
+ }
3149
+ }
3150
+ if (!tableExists) {
3151
+ const emptyMetrics = {};
3152
+ for (const token of activeTokens) {
3153
+ emptyMetrics[token.clientId] = {
3154
+ calls7d: 0,
3155
+ calls30d: 0,
3156
+ successRate: 0,
3157
+ avgDurationMs: 0,
3158
+ topTools: [],
3159
+ lastUsedAt: null
3160
+ };
3161
+ }
3162
+ return res.json({ metrics: emptyMetrics });
3163
+ }
3164
+ const now = /* @__PURE__ */ new Date();
3165
+ const sevenDaysAgo = new Date(now);
3166
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
3167
+ const thirtyDaysAgo = new Date(now);
3168
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
3169
+ const clientIdToDbId = /* @__PURE__ */ new Map();
3170
+ const dbIdToClientId = /* @__PURE__ */ new Map();
3171
+ const dbIds = [];
3172
+ for (const token of activeTokens) {
3173
+ clientIdToDbId.set(token.clientId, token.client.id);
3174
+ dbIdToClientId.set(token.client.id, token.clientId);
3175
+ dbIds.push(token.client.id);
3176
+ }
3177
+ const queryStartTime = Date.now();
3178
+ const metricsRaw = await prisma2.$queryRaw`
3179
+ SELECT
3180
+ oauth_client_id,
3181
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo})::bigint AS calls_7d,
3182
+ COUNT(*)::bigint AS calls_30d,
3183
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo} AND status = 'SUCCESS')::bigint AS success_count_7d,
3184
+ COUNT(*) FILTER (WHERE created_at >= ${sevenDaysAgo})::bigint AS total_count_7d,
3185
+ AVG(duration_ms) FILTER (WHERE created_at >= ${sevenDaysAgo})::numeric AS avg_duration_ms,
3186
+ MAX(created_at) AS last_used_at
3187
+ FROM tool_execution_logs
3188
+ WHERE tenant_id = ${tenantId}
3189
+ AND oauth_client_id = ANY(${dbIds})
3190
+ AND created_at >= ${thirtyDaysAgo}
3191
+ GROUP BY oauth_client_id
3192
+ `;
3193
+ const topToolsRaw = await prisma2.$queryRaw`
3194
+ WITH ranked_tools AS (
3195
+ SELECT
3196
+ oauth_client_id,
3197
+ tool_name,
3198
+ COUNT(*)::bigint AS call_count,
3199
+ ROW_NUMBER() OVER (PARTITION BY oauth_client_id ORDER BY COUNT(*) DESC) AS rn
3200
+ FROM tool_execution_logs
3201
+ WHERE tenant_id = ${tenantId}
3202
+ AND oauth_client_id = ANY(${dbIds})
3203
+ AND created_at >= ${sevenDaysAgo}
3204
+ GROUP BY oauth_client_id, tool_name
3205
+ )
3206
+ SELECT oauth_client_id, tool_name, call_count
3207
+ FROM ranked_tools
3208
+ WHERE rn <= 3
3209
+ `;
3210
+ const topToolsByDbId = /* @__PURE__ */ new Map();
3211
+ for (const row of topToolsRaw) {
3212
+ const existing = topToolsByDbId.get(row.oauth_client_id) ?? [];
3213
+ existing.push({ name: row.tool_name, calls: Number(row.call_count) });
3214
+ topToolsByDbId.set(row.oauth_client_id, existing);
3215
+ }
3216
+ const metricsMap = {};
3217
+ for (const token of activeTokens) {
3218
+ metricsMap[token.clientId] = {
3219
+ calls7d: 0,
3220
+ calls30d: 0,
3221
+ successRate: 0,
3222
+ avgDurationMs: 0,
3223
+ topTools: [],
3224
+ lastUsedAt: null
3225
+ };
3226
+ }
3227
+ for (const row of metricsRaw) {
3228
+ const externalClientId = dbIdToClientId.get(row.oauth_client_id);
3229
+ if (!externalClientId) continue;
3230
+ const totalCount = Number(row.total_count_7d);
3231
+ const successCount = Number(row.success_count_7d);
3232
+ const successRate = totalCount > 0 ? successCount / totalCount * 100 : 0;
3233
+ metricsMap[externalClientId] = {
3234
+ calls7d: Number(row.calls_7d),
3235
+ calls30d: Number(row.calls_30d),
3236
+ successRate: Math.round(successRate * 10) / 10,
3237
+ avgDurationMs: Math.round(row.avg_duration_ms ?? 0),
3238
+ topTools: topToolsByDbId.get(row.oauth_client_id) ?? [],
3239
+ lastUsedAt: row.last_used_at?.toISOString() ?? null
3240
+ };
3241
+ }
3242
+ const queryDuration = Date.now() - queryStartTime;
3243
+ if (queryDuration > 500) {
3244
+ logger7.warn("Batch OAuth client metrics query exceeded 500ms", {
3245
+ tenantId,
3246
+ clientCount: activeTokens.length,
3247
+ durationMs: queryDuration
3248
+ });
3249
+ }
3250
+ return res.json({ metrics: metricsMap });
3251
+ } catch (error) {
3252
+ logger7.error("batch metrics endpoint error", error, {
3253
+ correlationId: req.headers["x-correlation-id"]
3254
+ });
3255
+ return res.status(500).json({
3256
+ error: "Internal Server Error",
3257
+ message: "Failed to compute OAuth client metrics"
3258
+ });
3259
+ }
3260
+ });
3261
+ router.get("/:clientId/metrics", async (req, res) => {
3262
+ try {
3263
+ const tenantId = req.tenantContext?.tenantId;
3264
+ if (!tenantId) {
3265
+ return res.status(401).json({
3266
+ error: "Unauthorized",
3267
+ message: "Tenant context not found"
3268
+ });
3269
+ }
3270
+ const { clientId } = req.params;
3271
+ const clientRecord = await prisma2.oAuthRefreshToken.findFirst({
3272
+ where: {
3273
+ clientId,
3274
+ tenantId,
3275
+ revokedAt: null
3276
+ },
3277
+ include: {
3278
+ client: {
3279
+ select: { id: true }
3280
+ }
3281
+ }
3282
+ });
3283
+ if (!clientRecord) {
3284
+ return res.status(404).json({
3285
+ error: "Not Found",
3286
+ message: "OAuth client not found or no active tokens"
3287
+ });
3288
+ }
3289
+ const oauthClientDbId = clientRecord.client.id;
3290
+ let tableExists = true;
3291
+ try {
3292
+ await prisma2.$queryRaw`SELECT 1 FROM tool_execution_logs LIMIT 1`;
3293
+ } catch (error) {
3294
+ if (error instanceof Error && (error.message.includes("does not exist") || error.message.includes("relation") || error.message.includes("table"))) {
3295
+ tableExists = false;
3296
+ } else {
3297
+ throw error;
3298
+ }
3299
+ }
3300
+ if (!tableExists) {
3301
+ return res.json({
3302
+ calls7d: 0,
3303
+ calls30d: 0,
3304
+ successRate: 0,
3305
+ avgDurationMs: 0,
3306
+ topTools: [],
3307
+ lastUsedAt: null
3308
+ });
3309
+ }
3310
+ const now = /* @__PURE__ */ new Date();
3311
+ const sevenDaysAgo = new Date(now);
3312
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
3313
+ const thirtyDaysAgo = new Date(now);
3314
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
3315
+ const baseWhere = {
3316
+ tenantId,
3317
+ oauthClientId: oauthClientDbId
3318
+ };
3319
+ const queryStartTime = Date.now();
3320
+ const [calls7d, calls30d, totalCalls, successCount, avgDuration, topTools, lastUsed] = await Promise.all([
3321
+ // Calls in last 7 days
3322
+ prisma2.toolExecutionLog.count({
3323
+ where: {
3324
+ ...baseWhere,
3325
+ createdAt: { gte: sevenDaysAgo }
3326
+ }
3327
+ }),
3328
+ // Calls in last 30 days
3329
+ prisma2.toolExecutionLog.count({
3330
+ where: {
3331
+ ...baseWhere,
3332
+ createdAt: { gte: thirtyDaysAgo }
3333
+ }
3334
+ }),
3335
+ // Total calls (for success rate calculation)
3336
+ prisma2.toolExecutionLog.count({
3337
+ where: {
3338
+ ...baseWhere,
3339
+ createdAt: { gte: sevenDaysAgo }
3340
+ }
3341
+ }),
3342
+ // Success count (for success rate calculation)
3343
+ prisma2.toolExecutionLog.count({
3344
+ where: {
3345
+ ...baseWhere,
3346
+ createdAt: { gte: sevenDaysAgo },
3347
+ status: "SUCCESS"
3348
+ }
3349
+ }),
3350
+ // Average duration
3351
+ prisma2.toolExecutionLog.aggregate({
3352
+ where: {
3353
+ ...baseWhere,
3354
+ createdAt: { gte: sevenDaysAgo },
3355
+ durationMs: { not: null }
3356
+ },
3357
+ _avg: { durationMs: true }
3358
+ }),
3359
+ // Top 3 tools by call count in last 7 days
3360
+ prisma2.toolExecutionLog.groupBy({
3361
+ by: ["toolName"],
3362
+ where: {
3363
+ ...baseWhere,
3364
+ createdAt: { gte: sevenDaysAgo }
3365
+ },
3366
+ _count: { id: true },
3367
+ orderBy: { _count: { id: "desc" } },
3368
+ take: 3
3369
+ }),
3370
+ // Most recent execution
3371
+ prisma2.toolExecutionLog.findFirst({
3372
+ where: baseWhere,
3373
+ orderBy: { createdAt: "desc" },
3374
+ select: { createdAt: true }
3375
+ })
3376
+ ]);
3377
+ const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
3378
+ const topToolsFormatted = topTools.map((tool) => ({
3379
+ name: tool.toolName,
3380
+ calls: tool._count.id
3381
+ }));
3382
+ const queryDuration = Date.now() - queryStartTime;
3383
+ if (queryDuration > 200) {
3384
+ logger7.warn("OAuth client metrics query exceeded 200ms", {
3385
+ clientId,
3386
+ tenantId,
3387
+ durationMs: queryDuration
3388
+ });
3389
+ }
3390
+ const response = {
3391
+ calls7d,
3392
+ calls30d,
3393
+ successRate: Math.round(successRate * 10) / 10,
3394
+ // Round to 1 decimal place
3395
+ avgDurationMs: Math.round(avgDuration._avg.durationMs ?? 0),
3396
+ topTools: topToolsFormatted,
3397
+ lastUsedAt: lastUsed?.createdAt.toISOString() ?? null
3398
+ };
3399
+ return res.json(response);
3400
+ } catch (error) {
3401
+ logger7.error("metrics endpoint error", error, {
3402
+ correlationId: req.headers["x-correlation-id"]
3403
+ });
2673
3404
  return res.status(500).json({
2674
3405
  error: "Internal Server Error",
2675
- message: "An unexpected error occurred while listing OAuth clients"
3406
+ message: "Failed to compute OAuth client metrics"
2676
3407
  });
2677
3408
  }
2678
3409
  });
@@ -2733,7 +3464,10 @@ function createOAuthClientsRouter(options) {
2733
3464
  });
2734
3465
  return res.status(204).send();
2735
3466
  } catch (error) {
2736
- console.error("[OAUTH-CLIENTS API] Revocation error:", error);
3467
+ logger7.error("OAuth client revocation failed", error instanceof Error ? error : void 0, {
3468
+ tenantId: req.tenantContext?.tenantId,
3469
+ clientId: req.params.clientId
3470
+ });
2737
3471
  return res.status(500).json({
2738
3472
  error: "Internal Server Error",
2739
3473
  message: "An unexpected error occurred during revocation"
@@ -2826,7 +3560,7 @@ var oauthClientsRouter = createOAuthClientsRouter({
2826
3560
  });
2827
3561
 
2828
3562
  // src/lifecycle/shutdown.ts
2829
- var logger2 = createLogger("lifecycle/shutdown");
3563
+ var logger8 = createLogger("lifecycle/shutdown");
2830
3564
  var ShutdownManager = class {
2831
3565
  /** Current shutdown state */
2832
3566
  _state = "running";
@@ -2956,20 +3690,20 @@ var ShutdownManager = class {
2956
3690
  */
2957
3691
  registerSignalHandlers() {
2958
3692
  process.on("SIGTERM", () => {
2959
- logger2.info("Received SIGTERM signal");
3693
+ logger8.info("Received SIGTERM signal");
2960
3694
  this.initiateShutdown().catch((error) => {
2961
- logger2.error("Shutdown failed", error instanceof Error ? error : void 0);
3695
+ logger8.error("Shutdown failed", error instanceof Error ? error : void 0);
2962
3696
  process.exit(1);
2963
3697
  });
2964
3698
  });
2965
3699
  process.on("SIGINT", () => {
2966
- logger2.info("Received SIGINT signal");
3700
+ logger8.info("Received SIGINT signal");
2967
3701
  this.initiateShutdown().catch((error) => {
2968
- logger2.error("Shutdown failed", error instanceof Error ? error : void 0);
3702
+ logger8.error("Shutdown failed", error instanceof Error ? error : void 0);
2969
3703
  process.exit(1);
2970
3704
  });
2971
3705
  });
2972
- logger2.debug("Signal handlers registered (SIGTERM, SIGINT)");
3706
+ logger8.debug("Signal handlers registered (SIGTERM, SIGINT)");
2973
3707
  }
2974
3708
  /**
2975
3709
  * Initiate graceful shutdown
@@ -2987,13 +3721,13 @@ var ShutdownManager = class {
2987
3721
  */
2988
3722
  async initiateShutdown() {
2989
3723
  if (this.shutdownInProgress) {
2990
- logger2.debug("Shutdown already in progress, ignoring duplicate call");
3724
+ logger8.debug("Shutdown already in progress, ignoring duplicate call");
2991
3725
  return;
2992
3726
  }
2993
3727
  this.shutdownInProgress = true;
2994
3728
  this._state = "draining";
2995
3729
  this.drainingStartedAt = /* @__PURE__ */ new Date();
2996
- logger2.info("Shutdown initiated", {
3730
+ logger8.info("Shutdown initiated", {
2997
3731
  drainTimeoutMs: this.drainTimeoutMs,
2998
3732
  activeConnections: this.getRemainingConnections()
2999
3733
  });
@@ -3009,11 +3743,11 @@ var ShutdownManager = class {
3009
3743
  }
3010
3744
  if (this.mcpSessionsCleanup) {
3011
3745
  try {
3012
- logger2.debug("Closing MCP sessions");
3746
+ logger8.debug("Closing MCP sessions");
3013
3747
  await this.withTimeout(this.mcpSessionsCleanup(), 5e3, "MCP sessions cleanup");
3014
- logger2.debug("MCP sessions closed");
3748
+ logger8.debug("MCP sessions closed");
3015
3749
  } catch (error) {
3016
- logger2.error("Failed to close MCP sessions", error instanceof Error ? error : void 0);
3750
+ logger8.error("Failed to close MCP sessions", error instanceof Error ? error : void 0);
3017
3751
  }
3018
3752
  }
3019
3753
  const summary = await this.runCleanupCallbacks();
@@ -3031,14 +3765,14 @@ var ShutdownManager = class {
3031
3765
  resolve();
3032
3766
  return;
3033
3767
  }
3034
- logger2.debug("Stopping HTTP server from accepting new connections");
3768
+ logger8.debug("Stopping HTTP server from accepting new connections");
3035
3769
  this.httpServer.close((error) => {
3036
3770
  if (error) {
3037
3771
  if (error.code !== "ERR_SERVER_NOT_RUNNING") {
3038
- logger2.warn("HTTP server close error", { error: error.message });
3772
+ logger8.warn("HTTP server close error", { error: error.message });
3039
3773
  }
3040
3774
  }
3041
- logger2.debug("HTTP server stopped accepting connections");
3775
+ logger8.debug("HTTP server stopped accepting connections");
3042
3776
  resolve();
3043
3777
  });
3044
3778
  });
@@ -3051,7 +3785,7 @@ var ShutdownManager = class {
3051
3785
  while (this.getRemainingConnections() > 0) {
3052
3786
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
3053
3787
  }
3054
- logger2.debug("All connections drained");
3788
+ logger8.debug("All connections drained");
3055
3789
  }
3056
3790
  /**
3057
3791
  * Create drain timeout promise
@@ -3059,7 +3793,7 @@ var ShutdownManager = class {
3059
3793
  createDrainTimeout() {
3060
3794
  return new Promise((resolve) => {
3061
3795
  this.drainTimeoutTimer = setTimeout(() => {
3062
- logger2.warn("Drain timeout reached, proceeding with shutdown", {
3796
+ logger8.warn("Drain timeout reached, proceeding with shutdown", {
3063
3797
  remainingConnections: this.getRemainingConnections(),
3064
3798
  drainTimeoutMs: this.drainTimeoutMs
3065
3799
  });
@@ -3074,7 +3808,7 @@ var ShutdownManager = class {
3074
3808
  async runCleanupCallbacks() {
3075
3809
  const drainTimeMs = this.drainingStartedAt ? Date.now() - this.drainingStartedAt.getTime() : 0;
3076
3810
  let cleanupErrors = 0;
3077
- logger2.debug("Running cleanup callbacks", {
3811
+ logger8.debug("Running cleanup callbacks", {
3078
3812
  count: this.cleanupCallbacks.length
3079
3813
  });
3080
3814
  for (let i = 0; i < this.cleanupCallbacks.length; i++) {
@@ -3087,7 +3821,7 @@ var ShutdownManager = class {
3087
3821
  );
3088
3822
  } catch (error) {
3089
3823
  cleanupErrors++;
3090
- logger2.error(
3824
+ logger8.error(
3091
3825
  `Cleanup callback ${i + 1} failed`,
3092
3826
  error instanceof Error ? error : void 0
3093
3827
  );
@@ -3121,7 +3855,7 @@ var ShutdownManager = class {
3121
3855
  * Log final shutdown summary
3122
3856
  */
3123
3857
  logShutdownSummary(summary) {
3124
- logger2.info("Shutdown complete", {
3858
+ logger8.info("Shutdown complete", {
3125
3859
  drainTimeMs: summary.drainTimeMs,
3126
3860
  cleanupCallbacksRun: summary.cleanupCallbacksRun,
3127
3861
  cleanupErrors: summary.cleanupErrors,
@@ -3208,36 +3942,6 @@ var ShutdownManager = class {
3208
3942
  };
3209
3943
  var shutdownManager = new ShutdownManager();
3210
3944
 
3211
- // src/logging/correlation.ts
3212
- import { randomUUID as randomUUID2 } from "crypto";
3213
- var CORRELATION_ID_HEADER = "X-Correlation-ID";
3214
- var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3215
- function generateCorrelationId() {
3216
- return randomUUID2();
3217
- }
3218
- function isValidCorrelationId(id) {
3219
- return UUID_V4_REGEX.test(id);
3220
- }
3221
- function correlationMiddleware(req, res, next) {
3222
- const headerValue = req.headers[CORRELATION_ID_HEADER.toLowerCase()];
3223
- let correlationId;
3224
- if (headerValue && isValidCorrelationId(headerValue)) {
3225
- correlationId = headerValue;
3226
- } else {
3227
- correlationId = generateCorrelationId();
3228
- }
3229
- res.locals.correlationId = correlationId;
3230
- if (typeof res.set === "function") {
3231
- res.set(
3232
- CORRELATION_ID_HEADER,
3233
- correlationId
3234
- );
3235
- } else {
3236
- res.setHeader(CORRELATION_ID_HEADER, correlationId);
3237
- }
3238
- next();
3239
- }
3240
-
3241
3945
  // src/metrics/registry.ts
3242
3946
  import { Counter, Gauge, Histogram, Registry, collectDefaultMetrics } from "prom-client";
3243
3947
  var METRICS_PREFIX = "shopify_mcp_";
@@ -3920,10 +4624,10 @@ var OAuthDiscoveryService = class {
3920
4624
  // Used by ChatGPT for automatic client registration
3921
4625
  registration_endpoint: `${this.baseUrl}/oauth/register`,
3922
4626
  // Supported authentication methods at token endpoint
3923
- // Client authentication is required for token exchange
3924
4627
  // "client_secret_post" - credentials in request body (client_id, client_secret)
3925
4628
  // "client_secret_basic" - credentials in Authorization header (Basic auth)
3926
- token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
4629
+ // "none" - public client with PKCE only (required for Claude.ai)
4630
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic", "none"],
3927
4631
  // Supported grant types
3928
4632
  // authorization_code: Standard OAuth 2.0 flow with PKCE
3929
4633
  // refresh_token: For obtaining new access tokens
@@ -3931,6 +4635,9 @@ var OAuthDiscoveryService = class {
3931
4635
  // Response types for authorization endpoint
3932
4636
  // "code" for authorization code flow
3933
4637
  response_types_supported: ["code"],
4638
+ // Response modes for authorization endpoint
4639
+ // "query" returns code in URL query string (standard for auth code flow)
4640
+ response_modes_supported: ["query"],
3934
4641
  // PKCE code challenge methods supported (when PKCE is used)
3935
4642
  // S256 is the only method supported - plain is NOT allowed
3936
4643
  // Note: PKCE is optional for confidential clients (with client_secret)
@@ -4014,6 +4721,7 @@ var CLIENT_SECRET_BYTES = 16;
4014
4721
  var BCRYPT_COST_FACTOR2 = 12;
4015
4722
  var DEFAULT_GRANT_TYPES = ["authorization_code", "refresh_token"];
4016
4723
  var DEFAULT_SCOPES3 = ["mcp:full"];
4724
+ var PUBLIC_CLIENT_MARKER = "PUBLIC_CLIENT:none";
4017
4725
  var OAuthRegistrationService = class {
4018
4726
  prisma;
4019
4727
  /**
@@ -4084,22 +4792,40 @@ var OAuthRegistrationService = class {
4084
4792
  * hashed secret in the database, and returns RFC 7591 compliant
4085
4793
  * response with the plaintext secret (one-time reveal).
4086
4794
  *
4795
+ * Supports both confidential clients (with client_secret) and
4796
+ * public clients (token_endpoint_auth_method: "none", uses PKCE).
4797
+ *
4087
4798
  * @param request - RFC 7591 client registration request
4088
4799
  * @returns RFC 7591 compliant registration response
4089
4800
  *
4090
4801
  * @throws Error if database operation fails
4091
4802
  *
4092
4803
  * @example
4804
+ * // Confidential client (default)
4093
4805
  * const response = await service.registerClient({
4094
4806
  * client_name: "ChatGPT MCP Client",
4095
4807
  * redirect_uris: ["https://chatgpt.com/aip/g/callback"]
4096
4808
  * });
4097
- * // response.client_secret is only returned once!
4809
+ *
4810
+ * @example
4811
+ * // Public client (no secret, uses PKCE) - required for Claude.ai
4812
+ * const response = await service.registerClient({
4813
+ * client_name: "Claude AI",
4814
+ * redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
4815
+ * token_endpoint_auth_method: "none"
4816
+ * });
4098
4817
  */
4099
4818
  async registerClient(request) {
4100
4819
  const clientId = this.generateClientId();
4101
- const clientSecret = this.generateClientSecret();
4102
- const clientSecretHash = await this.hashClientSecret(clientSecret);
4820
+ const isPublicClient = request.token_endpoint_auth_method === "none";
4821
+ let clientSecret;
4822
+ let clientSecretHash;
4823
+ if (isPublicClient) {
4824
+ clientSecretHash = PUBLIC_CLIENT_MARKER;
4825
+ } else {
4826
+ clientSecret = this.generateClientSecret();
4827
+ clientSecretHash = await this.hashClientSecret(clientSecret);
4828
+ }
4103
4829
  const grantTypes = request.grant_types?.length ? request.grant_types : DEFAULT_GRANT_TYPES;
4104
4830
  const client = await this.prisma.oAuthClient.create({
4105
4831
  data: {
@@ -4111,10 +4837,8 @@ var OAuthRegistrationService = class {
4111
4837
  scopes: DEFAULT_SCOPES3
4112
4838
  }
4113
4839
  });
4114
- return {
4840
+ const response = {
4115
4841
  client_id: clientId,
4116
- client_secret: clientSecret,
4117
- // One-time reveal!
4118
4842
  client_name: request.client_name,
4119
4843
  redirect_uris: request.redirect_uris,
4120
4844
  grant_types: grantTypes,
@@ -4122,6 +4846,10 @@ var OAuthRegistrationService = class {
4122
4846
  client_secret_expires_at: 0
4123
4847
  // Per RFC 7591 Section 3.2.1: 0 means no expiry
4124
4848
  };
4849
+ if (clientSecret) {
4850
+ response.client_secret = clientSecret;
4851
+ }
4852
+ return response;
4125
4853
  }
4126
4854
  /**
4127
4855
  * Find a client by client_id
@@ -4632,15 +5360,16 @@ var OAuthTokenService = class {
4632
5360
  /**
4633
5361
  * Authenticate a client from request
4634
5362
  *
4635
- * Supports two authentication methods:
5363
+ * Supports three authentication methods:
4636
5364
  * - client_secret_post: client_id and client_secret in request body
4637
5365
  * - client_secret_basic: HTTP Basic auth header
5366
+ * - none: public client (no secret), requires PKCE - used by Claude.ai
4638
5367
  *
4639
5368
  * Per RFC 6749 Section 2.3.1
4640
5369
  *
4641
5370
  * @param params - Token request parameters (may contain client_id/secret)
4642
5371
  * @param authorizationHeader - Authorization header value (if present)
4643
- * @returns Authenticated client entity
5372
+ * @returns Authenticated client entity with isPublicClient flag
4644
5373
  * @throws OAuthTokenError if authentication fails
4645
5374
  */
4646
5375
  async authenticateClient(params, authorizationHeader) {
@@ -4675,15 +5404,18 @@ var OAuthTokenService = class {
4675
5404
  has_client_secret: !!clientSecret,
4676
5405
  secret_length: clientSecret?.length || 0
4677
5406
  });
4678
- if (!clientId || !clientSecret) {
4679
- log2.warn("Client authentication failed: missing credentials", {
4680
- has_client_id: !!clientId,
5407
+ if (!clientId) {
5408
+ clientId = params.client_id;
5409
+ }
5410
+ if (!clientId) {
5411
+ log2.warn("Client authentication failed: missing client_id", {
5412
+ has_client_id: false,
4681
5413
  has_client_secret: !!clientSecret,
4682
5414
  auth_method: authMethod
4683
5415
  });
4684
5416
  throw new OAuthTokenError(
4685
5417
  "invalid_client",
4686
- "Client authentication required. Provide client_id and client_secret.",
5418
+ "Client authentication required. Provide client_id.",
4687
5419
  401
4688
5420
  );
4689
5421
  }
@@ -4696,6 +5428,31 @@ var OAuthTokenService = class {
4696
5428
  });
4697
5429
  throw new OAuthTokenError("invalid_client", "Unknown client_id", 401);
4698
5430
  }
5431
+ const isPublicClient = client.clientSecretHash === PUBLIC_CLIENT_MARKER;
5432
+ if (isPublicClient) {
5433
+ log2.info("Public client authenticated (no secret required, PKCE enforced)", {
5434
+ client_id: clientId,
5435
+ client_name: client.clientName,
5436
+ auth_method: "none"
5437
+ });
5438
+ return {
5439
+ clientId: client.clientId,
5440
+ clientSecretHash: client.clientSecretHash,
5441
+ clientName: client.clientName,
5442
+ isPublicClient: true
5443
+ };
5444
+ }
5445
+ if (!clientSecret) {
5446
+ log2.warn("Client authentication failed: missing client_secret for confidential client", {
5447
+ client_id: clientId,
5448
+ client_name: client.clientName
5449
+ });
5450
+ throw new OAuthTokenError(
5451
+ "invalid_client",
5452
+ "Client authentication required. Provide client_id and client_secret.",
5453
+ 401
5454
+ );
5455
+ }
4699
5456
  const isValidSecret = await bcrypt5.compare(clientSecret, client.clientSecretHash);
4700
5457
  if (!isValidSecret) {
4701
5458
  log2.warn("Client authentication failed: invalid client_secret", {
@@ -4712,7 +5469,8 @@ var OAuthTokenService = class {
4712
5469
  return {
4713
5470
  clientId: client.clientId,
4714
5471
  clientSecretHash: client.clientSecretHash,
4715
- clientName: client.clientName
5472
+ clientName: client.clientName,
5473
+ isPublicClient: false
4716
5474
  };
4717
5475
  }
4718
5476
  // ===========================================================================
@@ -4850,6 +5608,19 @@ var OAuthTokenService = class {
4850
5608
  });
4851
5609
  throw new OAuthTokenError("invalid_grant", "Authorization code has already been used");
4852
5610
  }
5611
+ const client = await this.prisma.oAuthClient.findUnique({
5612
+ where: { clientId }
5613
+ });
5614
+ const isPublicClient = client?.clientSecretHash === PUBLIC_CLIENT_MARKER;
5615
+ if (isPublicClient && !authCode.codeChallenge) {
5616
+ log2.warn("Token exchange failed: public client did not use PKCE", {
5617
+ client_id: clientId
5618
+ });
5619
+ throw new OAuthTokenError(
5620
+ "invalid_grant",
5621
+ "Public clients must use PKCE. No code_challenge was provided during authorization."
5622
+ );
5623
+ }
4853
5624
  if (authCode.codeChallenge) {
4854
5625
  if (!params.code_verifier) {
4855
5626
  throw new OAuthTokenError(
@@ -5072,7 +5843,10 @@ var clientRegistrationRequestSchema = z6.object({
5072
5843
  invalid_type_error: "redirect_uris must be an array"
5073
5844
  }).min(1, "redirect_uris cannot be empty"),
5074
5845
  grant_types: z6.array(z6.string()).optional(),
5075
- response_types: z6.array(z6.string()).optional()
5846
+ response_types: z6.array(z6.string()).optional(),
5847
+ // Token endpoint authentication method (RFC 7591)
5848
+ // 'none' = public client (no client_secret, uses PKCE) - required for Claude.ai
5849
+ token_endpoint_auth_method: z6.enum(["client_secret_post", "client_secret_basic", "none"]).optional()
5076
5850
  });
5077
5851
  function validateClientRegistrationRequest(data) {
5078
5852
  const result = clientRegistrationRequestSchema.safeParse(data);
@@ -5101,6 +5875,7 @@ var TokenRefreshService = class {
5101
5875
  cryptoService;
5102
5876
  config;
5103
5877
  refreshInterval = null;
5878
+ clientPool = null;
5104
5879
  constructor(prisma2, cryptoService, config) {
5105
5880
  this.prisma = prisma2;
5106
5881
  this.cryptoService = cryptoService;
@@ -5112,6 +5887,14 @@ var TokenRefreshService = class {
5112
5887
  };
5113
5888
  log.debug("TokenRefreshService initialized");
5114
5889
  }
5890
+ /**
5891
+ * Set the client pool for cache eviction after token refresh
5892
+ * Must be called after both services are initialized to avoid circular dependency
5893
+ */
5894
+ setClientPool(clientPool) {
5895
+ this.clientPool = clientPool;
5896
+ log.debug("TokenRefreshService: client pool reference set");
5897
+ }
5115
5898
  /**
5116
5899
  * Refresh an access token using the refresh token
5117
5900
  *
@@ -5200,6 +5983,12 @@ var TokenRefreshService = class {
5200
5983
  where: { id: shop.id },
5201
5984
  data: updateData
5202
5985
  });
5986
+ if (this.clientPool) {
5987
+ this.clientPool.evict(shop.tenantId, shop.shopDomain);
5988
+ log.debug(
5989
+ `Evicted cached client for ${shop.shopDomain.substring(0, 20)} after token refresh`
5990
+ );
5991
+ }
5203
5992
  log.info(
5204
5993
  `Token refreshed successfully for ${shop.shopDomain.substring(0, 20)} (expires in ${tokenData.expires_in}s)`
5205
5994
  );
@@ -5214,6 +6003,18 @@ var TokenRefreshService = class {
5214
6003
  } catch (error) {
5215
6004
  const errorMessage = error instanceof Error ? error.message : "Unknown error during refresh";
5216
6005
  log.error(`Token refresh error: ${errorMessage}`);
6006
+ const isDecryptionFailure = errorMessage.includes("Decryption failed") || errorMessage.includes("invalid key") || errorMessage.includes("corrupted data");
6007
+ if (isDecryptionFailure) {
6008
+ log.error(
6009
+ `CRITICAL: Cannot decrypt tokens for shop ${shop.shopDomain.substring(0, 20)}. This likely means ENCRYPTION_KEY was changed. Shop must reconnect.`
6010
+ );
6011
+ await this.updateTokenStatus(shop.id, "needs_reauth" /* NEEDS_REAUTH */);
6012
+ return {
6013
+ success: false,
6014
+ error: `Token expired and refresh failed for shop ${shop.shopDomain.substring(0, 20)}.... Please reconnect the shop.`,
6015
+ status: "needs_reauth" /* NEEDS_REAUTH */
6016
+ };
6017
+ }
5217
6018
  return {
5218
6019
  success: false,
5219
6020
  error: errorMessage,
@@ -6047,6 +6848,104 @@ function createGDPRWebhooksRouter(options) {
6047
6848
  return router;
6048
6849
  }
6049
6850
 
6851
+ // src/transports/http/health-endpoints.ts
6852
+ var FAVICON_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR42u3NQQkAAAgEsItnQutYzRQ+hMH+S0+dikAgEAgEAoFAIPgSLM8edFuULS3fAAAAAElFTkSuQmCC";
6853
+ var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
6854
+ <rect width="32" height="32" rx="6" fill="#96BF48"/>
6855
+ <path d="M10 8h12l2 6v12a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V14l2-6z" fill="white" stroke="white" stroke-width="1"/>
6856
+ <path d="M12 8v-2a4 4 0 0 1 8 0v2" fill="none" stroke="#96BF48" stroke-width="2" stroke-linecap="round"/>
6857
+ </svg>`;
6858
+ function registerHealthEndpoints(app, config) {
6859
+ let oauthStatus = null;
6860
+ if (isRemoteMode(config)) {
6861
+ const shopifyClientId = config.SHOPIFY_CLIENT_ID;
6862
+ const shopifyClientSecret = config.SHOPIFY_CLIENT_SECRET;
6863
+ const appUrl = config.APP_URL || `http://localhost:${config.PORT}`;
6864
+ oauthStatus = {
6865
+ enabled: !!(shopifyClientId && shopifyClientSecret),
6866
+ hasClientId: !!shopifyClientId,
6867
+ hasClientSecret: !!shopifyClientSecret,
6868
+ hasAppUrl: !!config.APP_URL,
6869
+ redirectUri: shopifyClientId && shopifyClientSecret ? `${appUrl}/oauth/callback` : void 0
6870
+ };
6871
+ }
6872
+ app.get("/metrics", async (_req, res) => {
6873
+ if (!isMetricsEnabled(config)) {
6874
+ res.status(404).json({
6875
+ error: "Not Found",
6876
+ message: "Metrics endpoint is disabled"
6877
+ });
6878
+ return;
6879
+ }
6880
+ try {
6881
+ const metrics = await getMetricsOutput();
6882
+ res.setHeader("Content-Type", getMetricsContentType());
6883
+ res.send(metrics);
6884
+ } catch (error) {
6885
+ const message = error instanceof Error ? error.message : "Failed to collect metrics";
6886
+ log.error("Metrics collection failed", error instanceof Error ? error : void 0);
6887
+ res.status(500).json({
6888
+ error: "Internal Server Error",
6889
+ message
6890
+ });
6891
+ }
6892
+ });
6893
+ app.get("/health", async (_req, res) => {
6894
+ try {
6895
+ let drainingInfo;
6896
+ if (shutdownManager.isDraining()) {
6897
+ const info = shutdownManager.getDrainingInfo();
6898
+ if (info) {
6899
+ drainingInfo = {
6900
+ startedAt: info.startedAt,
6901
+ remainingConnections: shutdownManager.getRemainingConnections(),
6902
+ shutdownDeadline: info.shutdownDeadline
6903
+ };
6904
+ }
6905
+ }
6906
+ const health = await checkHealth({ drainingInfo });
6907
+ const statusCode = health.status === "draining" ? 503 : 200;
6908
+ log.debug(`Health check: ${health.status}`);
6909
+ const healthResponse = {
6910
+ ...health,
6911
+ ...isRemoteMode(config) && oauthStatus ? { oauth: oauthStatus } : {}
6912
+ };
6913
+ res.status(statusCode).json(healthResponse);
6914
+ } catch (error) {
6915
+ const message = error instanceof Error ? error.message : "Health check failed unexpectedly";
6916
+ log.warn(`Health check failed unexpectedly: ${message}`);
6917
+ res.status(503).json({
6918
+ status: "unhealthy",
6919
+ version: getVersion(),
6920
+ shopify: {
6921
+ connected: false,
6922
+ error: "Health check failed unexpectedly"
6923
+ },
6924
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6925
+ });
6926
+ }
6927
+ });
6928
+ app.get("/favicon.ico", (_req, res) => {
6929
+ const faviconBuffer = Buffer.from(FAVICON_PNG_BASE64, "base64");
6930
+ res.setHeader("Content-Type", "image/png");
6931
+ res.setHeader("Content-Length", faviconBuffer.length.toString());
6932
+ res.setHeader("Cache-Control", "public, max-age=86400");
6933
+ res.send(faviconBuffer);
6934
+ });
6935
+ app.get("/favicon.png", (_req, res) => {
6936
+ const faviconBuffer = Buffer.from(FAVICON_PNG_BASE64, "base64");
6937
+ res.setHeader("Content-Type", "image/png");
6938
+ res.setHeader("Content-Length", faviconBuffer.length.toString());
6939
+ res.setHeader("Cache-Control", "public, max-age=86400");
6940
+ res.send(faviconBuffer);
6941
+ });
6942
+ app.get("/favicon.svg", (_req, res) => {
6943
+ res.setHeader("Content-Type", "image/svg+xml");
6944
+ res.setHeader("Cache-Control", "public, max-age=86400");
6945
+ res.send(FAVICON_SVG);
6946
+ });
6947
+ }
6948
+
6050
6949
  // src/transports/http.ts
6051
6950
  async function createHttpTransport(server) {
6052
6951
  const app = express();
@@ -6062,7 +6961,7 @@ async function createHttpTransport(server) {
6062
6961
  }
6063
6962
  app.use(sentryRequestMiddleware);
6064
6963
  if (isRemoteMode(config)) {
6065
- const { createCorsMiddleware } = await import("./security-44M6F2QU.js");
6964
+ const { createCorsMiddleware } = await import("./security-6CNKRY2G.js");
6066
6965
  const { getAllowedOrigins } = await import("./schema-SOWYIQIV.js");
6067
6966
  const allowedOrigins = getAllowedOrigins(config);
6068
6967
  app.use(
@@ -6073,8 +6972,19 @@ async function createHttpTransport(server) {
6073
6972
  );
6074
6973
  log.info(`CORS middleware enabled with ${allowedOrigins.length} allowed origin(s)`);
6075
6974
  app.use((req, res, next) => {
6076
- const isOAuthPage = req.path.startsWith("/oauth/authorize") || req.path.startsWith("/app/oauth/");
6077
- const frameAncestors = isOAuthPage ? "frame-ancestors 'self' https://claude.ai https://*.anthropic.com" : "frame-ancestors 'none'";
6975
+ const isShopifyEmbeddedPage = req.path.startsWith("/app");
6976
+ const isOAuthRelatedPage = req.path.startsWith("/oauth/authorize") || req.path.startsWith("/app/oauth/") || req.path.startsWith("/app/login");
6977
+ let frameAncestors;
6978
+ if (isShopifyEmbeddedPage) {
6979
+ frameAncestors = "frame-ancestors 'self' https://admin.shopify.com https://*.myshopify.com https://claude.ai https://*.anthropic.com https://*.claude.ai";
6980
+ } else if (isOAuthRelatedPage) {
6981
+ frameAncestors = "frame-ancestors 'self' https://claude.ai https://*.anthropic.com https://*.claude.ai";
6982
+ } else {
6983
+ frameAncestors = "frame-ancestors 'none'";
6984
+ }
6985
+ if (isShopifyEmbeddedPage || isOAuthRelatedPage) {
6986
+ res.locals.allowIframeEmbedding = true;
6987
+ }
6078
6988
  res.setHeader(
6079
6989
  "Content-Security-Policy",
6080
6990
  [
@@ -6086,7 +6996,8 @@ async function createHttpTransport(server) {
6086
6996
  "connect-src 'self' https://*.myshopify.com",
6087
6997
  frameAncestors,
6088
6998
  "base-uri 'self'",
6089
- "form-action 'self' https://claude.ai"
6999
+ // Allow form submissions to Claude and Anthropic domains for OAuth redirects
7000
+ "form-action 'self' https://claude.ai https://*.claude.ai https://*.anthropic.com"
6090
7001
  ].join("; ")
6091
7002
  );
6092
7003
  next();
@@ -6097,20 +7008,61 @@ async function createHttpTransport(server) {
6097
7008
  let clientPool = null;
6098
7009
  let auditLogger2 = null;
6099
7010
  let cryptoService = null;
7011
+ let keepAliveInterval = null;
6100
7012
  if (isRemoteMode(config)) {
6101
7013
  log.info("Remote mode: enabling tenant onboarding API routes");
7014
+ try {
7015
+ await warmupDatabase();
7016
+ } catch (error) {
7017
+ log.error("Database warmup failed, server may experience cold-start delays");
7018
+ }
6102
7019
  const prisma2 = getPrismaClient();
6103
7020
  const encryptionKey = requireEncryptionKey(config);
6104
7021
  cryptoService = new CredentialEncryptionService(encryptionKey);
6105
7022
  auditLogger2 = new AuditLogger(prisma2);
7023
+ getToolExecutionLogger(prisma2);
7024
+ log.info("ToolExecutionLogger initialized for activity logging");
6106
7025
  apiKeyService = new ApiKeyService(prisma2);
6107
7026
  clientPool = new TenantClientPool(cryptoService, prisma2);
6108
7027
  log.info("TenantClientPool initialized for multi-tenant MCP");
7028
+ try {
7029
+ const validationResult = await cryptoService.validateEncryptionKeyOnStartup(prisma2);
7030
+ if (validationResult.totalCount > 0) {
7031
+ if (validationResult.failedCount > 0) {
7032
+ log.error(
7033
+ `CRITICAL: ENCRYPTION_KEY MISMATCH DETECTED! ${validationResult.failedCount}/${validationResult.totalCount} shop tokens cannot be decrypted. Affected shops will need to reconnect.`
7034
+ );
7035
+ captureError(
7036
+ new Error(
7037
+ `Encryption key mismatch: ${validationResult.failedCount} tokens cannot be decrypted`
7038
+ ),
7039
+ {
7040
+ totalCount: validationResult.totalCount,
7041
+ failedCount: validationResult.failedCount,
7042
+ failedShopIds: validationResult.failedShopIds
7043
+ }
7044
+ );
7045
+ await cryptoService.markShopsAsNeedingReauth(prisma2, validationResult.failedShopIds);
7046
+ log.warn(`Marked ${validationResult.failedCount} shops as needing re-authentication`);
7047
+ } else {
7048
+ log.info(
7049
+ `Encryption key validation passed: ${validationResult.successCount} shop tokens verified`
7050
+ );
7051
+ }
7052
+ }
7053
+ } catch (validationError) {
7054
+ log.error(
7055
+ `Failed to validate encryption key on startup: ${validationError instanceof Error ? validationError.message : "Unknown"}`
7056
+ );
7057
+ }
6109
7058
  if (config.SHOPIFY_CLIENT_ID && config.SHOPIFY_CLIENT_SECRET) {
6110
7059
  const tokenRefreshService = initializeTokenRefreshService(prisma2, cryptoService, {
6111
7060
  clientId: config.SHOPIFY_CLIENT_ID,
6112
7061
  clientSecret: config.SHOPIFY_CLIENT_SECRET
6113
7062
  });
7063
+ if (clientPool) {
7064
+ tokenRefreshService.setClientPool(clientPool);
7065
+ }
6114
7066
  tokenRefreshService.startBackgroundRefresh();
6115
7067
  log.info("TokenRefreshService initialized (background refresh enabled)");
6116
7068
  } else {
@@ -6205,6 +7157,50 @@ async function createHttpTransport(server) {
6205
7157
  }
6206
7158
  next();
6207
7159
  });
7160
+ app.head("/mcp", async (req, res) => {
7161
+ const authHeader = req.headers.authorization;
7162
+ const bearerToken = typeof authHeader === "string" && authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : void 0;
7163
+ log.info("[mcp] HEAD /mcp probe request", {
7164
+ hasAuth: !!bearerToken,
7165
+ userAgent: req.headers["user-agent"]
7166
+ });
7167
+ if (!bearerToken) {
7168
+ const wwwAuth2 = oauthDiscoveryService.getWwwAuthenticateHeader(
7169
+ "invalid_token",
7170
+ "Authentication required"
7171
+ );
7172
+ res.setHeader("WWW-Authenticate", wwwAuth2);
7173
+ res.status(401).json({
7174
+ error: "unauthorized",
7175
+ message: "Authentication required. Initiate OAuth flow."
7176
+ });
7177
+ return;
7178
+ }
7179
+ if (apiKeyService) {
7180
+ try {
7181
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
7182
+ const authResult = await validateMcpBearerToken(
7183
+ `Bearer ${bearerToken}`,
7184
+ apiKeyService,
7185
+ getPrismaClient()
7186
+ );
7187
+ if (authResult.valid) {
7188
+ res.status(200).end();
7189
+ return;
7190
+ }
7191
+ } catch {
7192
+ }
7193
+ }
7194
+ const wwwAuth = oauthDiscoveryService.getWwwAuthenticateHeader(
7195
+ "invalid_token",
7196
+ "Invalid or expired token"
7197
+ );
7198
+ res.setHeader("WWW-Authenticate", wwwAuth);
7199
+ res.status(401).json({
7200
+ error: "unauthorized",
7201
+ message: "Invalid or expired token"
7202
+ });
7203
+ });
6208
7204
  app.post("/mcp", async (req, res) => {
6209
7205
  try {
6210
7206
  const authHeader = req.headers.authorization;
@@ -6252,7 +7248,7 @@ async function createHttpTransport(server) {
6252
7248
  }
6253
7249
  if (sessionContext && methodRequiresShop && isRemote && apiKeyService && clientPool && prisma2 && (!sessionContext.requestContext?.client || !sessionContext.requestContext?.shopDomain)) {
6254
7250
  log.debug(`[mcp] Lazy loading context for existing session: ${sessionPrefix}...`);
6255
- const { validateMcpBearerToken } = await import("./mcp-auth-F25V6FEY.js");
7251
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6256
7252
  const authResult = await validateMcpBearerToken(
6257
7253
  req.headers.authorization,
6258
7254
  apiKeyService,
@@ -6271,7 +7267,12 @@ async function createHttpTransport(server) {
6271
7267
  authResult.tenant.defaultShop.domain,
6272
7268
  authResult.tenant.tenantId,
6273
7269
  authResult.tenant.apiKeyId || "",
6274
- authResult.tenant.allowedShops
7270
+ authResult.tenant.allowedShops,
7271
+ {
7272
+ oauthClientId: authResult.tenant.oauthClientId,
7273
+ oauthClientName: authResult.tenant.oauthClientName,
7274
+ correlationId
7275
+ }
6275
7276
  ),
6276
7277
  correlationId
6277
7278
  };
@@ -6356,7 +7357,7 @@ async function createHttpTransport(server) {
6356
7357
  let shopifyClient;
6357
7358
  let multiTenantContext;
6358
7359
  if (isRemote && apiKeyService && clientPool && prisma2) {
6359
- const { validateMcpBearerToken } = await import("./mcp-auth-F25V6FEY.js");
7360
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6360
7361
  const authResult = await validateMcpBearerToken(
6361
7362
  req.headers.authorization,
6362
7363
  apiKeyService,
@@ -6437,7 +7438,12 @@ async function createHttpTransport(server) {
6437
7438
  mcpTenantContext.defaultShop.domain,
6438
7439
  mcpTenantContext.tenantId,
6439
7440
  mcpTenantContext.apiKeyId || "",
6440
- mcpTenantContext.allowedShops
7441
+ mcpTenantContext.allowedShops,
7442
+ {
7443
+ oauthClientId: mcpTenantContext.oauthClientId,
7444
+ oauthClientName: mcpTenantContext.oauthClientName,
7445
+ correlationId
7446
+ }
6441
7447
  ),
6442
7448
  correlationId
6443
7449
  };
@@ -6447,7 +7453,7 @@ async function createHttpTransport(server) {
6447
7453
  `[mcp] Creating new transport: isInitReq=${isInitReq}, has_tenant=${!!mcpTenantContext}, has_client=${!!shopifyClient}`
6448
7454
  );
6449
7455
  transport = new StreamableHTTPServerTransport({
6450
- sessionIdGenerator: () => randomUUID3(),
7456
+ sessionIdGenerator: () => randomUUID2(),
6451
7457
  onsessioninitialized: (newSessionId) => {
6452
7458
  log.info(`[mcp] Session initialized: ${newSessionId.substring(0, 8)}...`);
6453
7459
  transports.set(newSessionId, transport);
@@ -6500,7 +7506,7 @@ async function createHttpTransport(server) {
6500
7506
  });
6501
7507
  await server.connect(transport);
6502
7508
  log.info("[mcp] Transport connected to server, handling request...");
6503
- const tempContextKey = multiTenantContext?.correlationId || randomUUID3();
7509
+ const tempContextKey = multiTenantContext?.correlationId || randomUUID2();
6504
7510
  try {
6505
7511
  if (multiTenantContext) {
6506
7512
  setFallbackContext(tempContextKey, multiTenantContext);
@@ -6639,7 +7645,7 @@ async function createHttpTransport(server) {
6639
7645
  let shopifyClient;
6640
7646
  let multiTenantContext;
6641
7647
  if (isRemote && apiKeyService && clientPool && prisma2) {
6642
- const { validateMcpBearerToken } = await import("./mcp-auth-F25V6FEY.js");
7648
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6643
7649
  const authResult = await validateMcpBearerToken(
6644
7650
  req.headers.authorization,
6645
7651
  apiKeyService,
@@ -6804,7 +7810,7 @@ data: ${JSON.stringify({
6804
7810
  const isRemote = isRemoteMode(config);
6805
7811
  const prisma2 = isRemote ? getPrismaClient() : null;
6806
7812
  if (isRemote && apiKeyService && prisma2) {
6807
- const { validateMcpBearerToken } = await import("./mcp-auth-F25V6FEY.js");
7813
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6808
7814
  const authResult = await validateMcpBearerToken(
6809
7815
  req.headers.authorization,
6810
7816
  apiKeyService,
@@ -6855,7 +7861,7 @@ data: ${JSON.stringify({
6855
7861
  return;
6856
7862
  }
6857
7863
  if (body.method === "tools/list") {
6858
- const { getRegisteredTools } = await import("./tools-HVUCP53D.js");
7864
+ const { getRegisteredTools } = await import("./tools-SVKPHJYW.js");
6859
7865
  const tools = getRegisteredTools();
6860
7866
  const response = {
6861
7867
  jsonrpc: "2.0",
@@ -6893,7 +7899,7 @@ data: ${JSON.stringify({
6893
7899
  return;
6894
7900
  }
6895
7901
  if (body.method === "tools/call" && isRemote && apiKeyService && clientPool && prisma2) {
6896
- const { validateMcpBearerToken } = await import("./mcp-auth-F25V6FEY.js");
7902
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6897
7903
  const authResult = await validateMcpBearerToken(
6898
7904
  req.headers.authorization,
6899
7905
  apiKeyService,
@@ -6950,7 +7956,7 @@ data: ${JSON.stringify({
6950
7956
  ),
6951
7957
  correlationId
6952
7958
  };
6953
- const { getToolByName } = await import("./tools-HVUCP53D.js");
7959
+ const { getToolByName } = await import("./tools-SVKPHJYW.js");
6954
7960
  const params = body.params;
6955
7961
  const rawToolName = params?.name;
6956
7962
  const toolArgs = params?.arguments || {};
@@ -7074,27 +8080,7 @@ data: ${JSON.stringify({
7074
8080
  const queryString = Object.keys(req.query).length ? `?${new URLSearchParams(req.query).toString()}` : "";
7075
8081
  res.redirect(307, `/sse${queryString}`);
7076
8082
  });
7077
- app.get("/metrics", async (_req, res) => {
7078
- if (!isMetricsEnabled(config)) {
7079
- res.status(404).json({
7080
- error: "Not Found",
7081
- message: "Metrics endpoint is disabled"
7082
- });
7083
- return;
7084
- }
7085
- try {
7086
- const metrics = await getMetricsOutput();
7087
- res.setHeader("Content-Type", getMetricsContentType());
7088
- res.send(metrics);
7089
- } catch (error) {
7090
- const message = error instanceof Error ? error.message : "Failed to collect metrics";
7091
- log.error("Metrics collection failed", error instanceof Error ? error : void 0);
7092
- res.status(500).json({
7093
- error: "Internal Server Error",
7094
- message
7095
- });
7096
- }
7097
- });
8083
+ registerHealthEndpoints(app, config);
7098
8084
  let oauthStatus = null;
7099
8085
  if (isRemoteMode(config)) {
7100
8086
  const shopifyClientId = config.SHOPIFY_CLIENT_ID;
@@ -7108,41 +8094,6 @@ data: ${JSON.stringify({
7108
8094
  redirectUri: shopifyClientId && shopifyClientSecret ? `${appUrl}/oauth/callback` : void 0
7109
8095
  };
7110
8096
  }
7111
- app.get("/health", async (_req, res) => {
7112
- try {
7113
- let drainingInfo;
7114
- if (shutdownManager.isDraining()) {
7115
- const info = shutdownManager.getDrainingInfo();
7116
- if (info) {
7117
- drainingInfo = {
7118
- startedAt: info.startedAt,
7119
- remainingConnections: shutdownManager.getRemainingConnections(),
7120
- shutdownDeadline: info.shutdownDeadline
7121
- };
7122
- }
7123
- }
7124
- const health = await checkHealth({ drainingInfo });
7125
- const statusCode = health.status === "draining" ? 503 : 200;
7126
- log.debug(`Health check: ${health.status}`);
7127
- const healthResponse = {
7128
- ...health,
7129
- ...isRemoteMode(config) && oauthStatus ? { oauth: oauthStatus } : {}
7130
- };
7131
- res.status(statusCode).json(healthResponse);
7132
- } catch (error) {
7133
- const message = error instanceof Error ? error.message : "Health check failed unexpectedly";
7134
- log.warn(`Health check failed unexpectedly: ${message}`);
7135
- res.status(503).json({
7136
- status: "unhealthy",
7137
- version: getVersion(),
7138
- shopify: {
7139
- connected: false,
7140
- error: "Health check failed unexpectedly"
7141
- },
7142
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7143
- });
7144
- }
7145
- });
7146
8097
  const oauthDiscoveryService = createOAuthDiscoveryService(discoveryBaseUrl);
7147
8098
  app.get("/.well-known/oauth-authorization-server", (_req, res) => {
7148
8099
  const metadata = oauthDiscoveryService.getAuthorizationServerMetadata();
@@ -7184,18 +8135,43 @@ data: ${JSON.stringify({
7184
8135
  const oauthAuthorizeService = createOAuthAuthorizeService(void 0, cryptoService || void 0);
7185
8136
  app.get("/oauth/authorize", async (req, res) => {
7186
8137
  try {
8138
+ const userAgent = req.headers["user-agent"] || "unknown";
8139
+ const origin = req.headers.origin || req.headers.referer || "none";
8140
+ const clientId = req.query.client_id;
8141
+ log.info("[OAuth] GET /oauth/authorize request", {
8142
+ client_id: clientId?.substring(0, 20),
8143
+ has_session_cookie: !!req.cookies?.session_id,
8144
+ origin: typeof origin === "string" ? origin.substring(0, 50) : "none",
8145
+ user_agent: userAgent.substring(0, 80),
8146
+ redirect_uri: req.query.redirect_uri?.substring(0, 80),
8147
+ // Check if this looks like a Claude request
8148
+ is_claude_origin: origin.includes("claude.ai") || origin.includes("anthropic.com")
8149
+ });
7187
8150
  const sessionId = req.cookies?.session_id;
7188
8151
  if (!sessionId) {
8152
+ log.info("[OAuth] No session cookie - redirecting to login", {
8153
+ client_id: clientId?.substring(0, 20),
8154
+ is_iframe: req.headers["sec-fetch-dest"] === "iframe"
8155
+ });
7189
8156
  const returnUrl = `/oauth/authorize?${new URLSearchParams(req.query).toString()}`;
7190
8157
  res.redirect(`/app/login?redirect=${encodeURIComponent(returnUrl)}`);
7191
8158
  return;
7192
8159
  }
7193
8160
  const session = await sessionStore.get(sessionId);
7194
8161
  if (!session || session.expiresAt < Date.now()) {
8162
+ log.info("[OAuth] Session expired or invalid - redirecting to login", {
8163
+ client_id: clientId?.substring(0, 20),
8164
+ session_exists: !!session,
8165
+ expired: session ? session.expiresAt < Date.now() : "N/A"
8166
+ });
7195
8167
  const returnUrl = `/oauth/authorize?${new URLSearchParams(req.query).toString()}`;
7196
8168
  res.redirect(`/app/login?redirect=${encodeURIComponent(returnUrl)}`);
7197
8169
  return;
7198
8170
  }
8171
+ log.info("[OAuth] Valid session found, proceeding with authorization", {
8172
+ client_id: clientId?.substring(0, 20),
8173
+ tenant_id: session.tenantId.substring(0, 8)
8174
+ });
7199
8175
  const validationResult = await oauthAuthorizeService.validateAuthorizationRequest(
7200
8176
  req.query
7201
8177
  );
@@ -7748,6 +8724,13 @@ data: ${JSON.stringify({
7748
8724
  log.debug("Disconnecting Prisma");
7749
8725
  await disconnectPrisma();
7750
8726
  });
8727
+ shutdownManager.onShutdown(() => {
8728
+ if (keepAliveInterval) {
8729
+ log.debug("Stopping database keep-alive");
8730
+ clearInterval(keepAliveInterval);
8731
+ keepAliveInterval = null;
8732
+ }
8733
+ });
7751
8734
  }
7752
8735
  return {
7753
8736
  app,
@@ -7756,6 +8739,21 @@ data: ${JSON.stringify({
7756
8739
  shutdownManager.setHttpServer(httpServer);
7757
8740
  shutdownManager.registerSignalHandlers();
7758
8741
  log.info("Graceful shutdown handlers registered");
8742
+ if (isRemoteMode(config)) {
8743
+ const KEEP_ALIVE_INTERVAL_MS = 4 * 60 * 1e3;
8744
+ keepAliveInterval = setInterval(async () => {
8745
+ try {
8746
+ const prisma2 = getPrismaClient();
8747
+ await prisma2.$queryRaw`SELECT 1`;
8748
+ log.debug("Database keep-alive ping successful");
8749
+ } catch (error) {
8750
+ const message = error instanceof Error ? error.message : "Unknown error";
8751
+ log.warn(`Database keep-alive ping failed: ${message}`);
8752
+ }
8753
+ }, KEEP_ALIVE_INTERVAL_MS);
8754
+ keepAliveInterval.unref();
8755
+ log.info("Database keep-alive started (interval: 4 minutes)");
8756
+ }
7759
8757
  return httpServer;
7760
8758
  }
7761
8759
  };