@anton.andrusenko/shopify-mcp-admin 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ captureError,
3
4
  clearFallbackContext,
4
5
  correlationMiddleware,
5
6
  createLogger,
@@ -18,6 +19,7 @@ import {
18
19
  getStorePolicies,
19
20
  getStoreShipping,
20
21
  getStoreTaxes,
22
+ getToolExecutionLogger,
21
23
  initSentry,
22
24
  isMultiTenantContext,
23
25
  registerAllTools,
@@ -28,21 +30,22 @@ import {
28
30
  setCurrentContextKey,
29
31
  setFallbackContext,
30
32
  validateShopifyToken
31
- } from "./chunk-RBXQOPVF.js";
33
+ } from "./chunk-UMNIRP6T.js";
32
34
  import {
33
35
  disconnectPrisma,
34
36
  getPrismaClient,
35
37
  prisma,
36
- sessionStore
37
- } from "./chunk-JU5IFCVJ.js";
38
+ sessionStore,
39
+ warmupDatabase
40
+ } from "./chunk-CJXPHNYT.js";
38
41
  import {
39
42
  createJsonRpcError
40
- } from "./chunk-EQUN4XCH.js";
43
+ } from "./chunk-H36XQ6QK.js";
41
44
  import {
42
45
  getConfig,
43
46
  log,
44
47
  sanitizeLogMessage
45
- } from "./chunk-5QMYOO4B.js";
48
+ } from "./chunk-CZJ7LSEO.js";
46
49
  import {
47
50
  getShutdownDrainMs,
48
51
  isLazyLoadingEnabled,
@@ -1191,6 +1194,7 @@ var ApiKeyService = class {
1191
1194
  };
1192
1195
 
1193
1196
  // src/api/tenants.ts
1197
+ var logger2 = createLogger("api/tenants");
1194
1198
  var BCRYPT_COST_FACTOR = 12;
1195
1199
  var INITIAL_API_KEY_NAME = "Initial API Key";
1196
1200
  var registerSchema = z.object({
@@ -1307,7 +1311,7 @@ function createTenantsRouter(options) {
1307
1311
  };
1308
1312
  return res.status(201).json(response);
1309
1313
  } catch (error) {
1310
- console.error("[TENANT API] Registration error:", error);
1314
+ logger2.error("Tenant registration failed", error instanceof Error ? error : void 0);
1311
1315
  return res.status(500).json({
1312
1316
  error: "Internal Server Error",
1313
1317
  message: "An unexpected error occurred during registration"
@@ -1347,7 +1351,9 @@ function createTenantsRouter(options) {
1347
1351
  createdAt: tenant.createdAt.toISOString()
1348
1352
  });
1349
1353
  } catch (error) {
1350
- 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
+ });
1351
1357
  return res.status(500).json({
1352
1358
  error: "Internal Server Error",
1353
1359
  message: "An unexpected error occurred"
@@ -1411,7 +1417,9 @@ function createTenantsRouter(options) {
1411
1417
  updatedAt: updatedTenant.updatedAt.toISOString()
1412
1418
  });
1413
1419
  } catch (error) {
1414
- 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
+ });
1415
1423
  return res.status(500).json({
1416
1424
  error: "Internal Server Error",
1417
1425
  message: "An unexpected error occurred"
@@ -1460,7 +1468,7 @@ function createTenantsRouter(options) {
1460
1468
  where: { id: tenantId },
1461
1469
  data: { passwordHash: newPasswordHash }
1462
1470
  });
1463
- const { sessionStore: sessionStore2 } = await import("./store-JK2ZU6DR.js");
1471
+ const { sessionStore: sessionStore2 } = await import("./store-5NJBYK45.js");
1464
1472
  await sessionStore2.deleteByTenantId(tenantId);
1465
1473
  await auditLogger2.log(tenantId, {
1466
1474
  action: "tenant.password_change",
@@ -1469,7 +1477,9 @@ function createTenantsRouter(options) {
1469
1477
  });
1470
1478
  return res.status(204).send();
1471
1479
  } catch (error) {
1472
- 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
+ });
1473
1483
  return res.status(500).json({
1474
1484
  error: "Internal Server Error",
1475
1485
  message: "An unexpected error occurred"
@@ -1641,9 +1651,67 @@ var CredentialEncryptionService = class _CredentialEncryptionService {
1641
1651
  }
1642
1652
  return rotatedCount;
1643
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
+ }
1644
1711
  };
1645
1712
 
1646
1713
  // src/api/shops.ts
1714
+ var logger3 = createLogger("api/shops");
1647
1715
  var SHOP_DOMAIN_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9-]*\.myshopify\.com$/;
1648
1716
  var ACCESS_TOKEN_PATTERN = /^shp(at|ua)_[a-f0-9]+$/i;
1649
1717
  var manualConnectSchema = z2.object({
@@ -1759,7 +1827,9 @@ function createShopsRouter(options) {
1759
1827
  };
1760
1828
  return res.status(201).json(response);
1761
1829
  } catch (error) {
1762
- 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
+ });
1763
1833
  return res.status(500).json({
1764
1834
  error: "Internal Server Error",
1765
1835
  message: "An unexpected error occurred during shop connection"
@@ -1818,7 +1888,9 @@ function createShopsRouter(options) {
1818
1888
  });
1819
1889
  return res.json({ shops: shopList });
1820
1890
  } catch (error) {
1821
- 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
+ });
1822
1894
  return res.status(500).json({
1823
1895
  error: "Internal Server Error",
1824
1896
  message: "An unexpected error occurred while listing shops"
@@ -1871,7 +1943,10 @@ function createShopsRouter(options) {
1871
1943
  });
1872
1944
  return res.status(204).send();
1873
1945
  } catch (error) {
1874
- 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
+ });
1875
1950
  return res.status(500).json({
1876
1951
  error: "Internal Server Error",
1877
1952
  message: "An unexpected error occurred while disconnecting shop"
@@ -1884,7 +1959,7 @@ function createShopsRouter(options) {
1884
1959
  // src/api/keys.ts
1885
1960
  import { Router as Router3 } from "express";
1886
1961
  import { z as z3 } from "zod";
1887
- var logger2 = createLogger("api/keys");
1962
+ var logger4 = createLogger("api/keys");
1888
1963
  var DEFAULT_SCOPES = ["*"];
1889
1964
  var createKeySchema = z3.object({
1890
1965
  name: z3.string().min(1, "Name is required").max(100, "Name must be 100 characters or less"),
@@ -1938,7 +2013,9 @@ function createKeysRouter(options) {
1938
2013
  };
1939
2014
  return res.status(201).json(response);
1940
2015
  } catch (error) {
1941
- 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
+ });
1942
2019
  return res.status(500).json({
1943
2020
  error: "Internal Server Error",
1944
2021
  message: "An unexpected error occurred during key creation"
@@ -1976,13 +2053,120 @@ function createKeysRouter(options) {
1976
2053
  }));
1977
2054
  return res.json({ keys: keyList });
1978
2055
  } catch (error) {
1979
- 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
+ });
1980
2059
  return res.status(500).json({
1981
2060
  error: "Internal Server Error",
1982
2061
  message: "An unexpected error occurred while listing keys"
1983
2062
  });
1984
2063
  }
1985
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
+ });
1986
2170
  router.get("/:keyId/metrics", async (req, res) => {
1987
2171
  try {
1988
2172
  const tenantId = req.tenantContext?.tenantId;
@@ -2007,6 +2191,25 @@ function createKeysRouter(options) {
2007
2191
  message: "API key not found"
2008
2192
  });
2009
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
+ }
2010
2213
  const now = /* @__PURE__ */ new Date();
2011
2214
  const sevenDaysAgo = new Date(now);
2012
2215
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
@@ -2066,7 +2269,7 @@ function createKeysRouter(options) {
2066
2269
  const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
2067
2270
  const queryDuration = Date.now() - queryStartTime;
2068
2271
  if (queryDuration > 200) {
2069
- logger2.warn("API key metrics query exceeded 200ms", {
2272
+ logger4.warn("API key metrics query exceeded 200ms", {
2070
2273
  keyId,
2071
2274
  tenantId,
2072
2275
  durationMs: queryDuration
@@ -2082,7 +2285,7 @@ function createKeysRouter(options) {
2082
2285
  };
2083
2286
  return res.json(response);
2084
2287
  } catch (error) {
2085
- logger2.error("metrics endpoint error", error, {
2288
+ logger4.error("metrics endpoint error", error, {
2086
2289
  correlationId: req.headers["x-correlation-id"]
2087
2290
  });
2088
2291
  return res.status(500).json({
@@ -2125,7 +2328,10 @@ function createKeysRouter(options) {
2125
2328
  });
2126
2329
  return res.status(204).send();
2127
2330
  } catch (error) {
2128
- 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
+ });
2129
2335
  return res.status(500).json({
2130
2336
  error: "Internal Server Error",
2131
2337
  message: "An unexpected error occurred during key revocation"
@@ -2143,6 +2349,7 @@ import bcrypt3 from "bcrypt";
2143
2349
  import { Router as Router4 } from "express";
2144
2350
  import rateLimit2 from "express-rate-limit";
2145
2351
  import { z as z4 } from "zod";
2352
+ var logger5 = createLogger("api/auth");
2146
2353
  var loginSchema = z4.object({
2147
2354
  email: z4.string().email("Invalid email format"),
2148
2355
  password: z4.string().min(1, "Password is required")
@@ -2261,7 +2468,7 @@ function createAuthRouter(_config) {
2261
2468
  }
2262
2469
  });
2263
2470
  } catch (error) {
2264
- console.error("[POST /auth/login] Unexpected error:", error);
2471
+ logger5.error("Login failed", error instanceof Error ? error : void 0);
2265
2472
  res.status(500).json({
2266
2473
  error: "Internal Server Error",
2267
2474
  message: "An unexpected error occurred"
@@ -2325,7 +2532,7 @@ function createAuthRouter(_config) {
2325
2532
  }
2326
2533
  res.status(204).send();
2327
2534
  } catch (error) {
2328
- console.error("[POST /auth/logout] Unexpected error:", error);
2535
+ logger5.error("Logout failed", error instanceof Error ? error : void 0);
2329
2536
  res.status(500).json({
2330
2537
  error: "Internal Server Error",
2331
2538
  message: "An unexpected error occurred"
@@ -2338,7 +2545,7 @@ function createAuthRouter(_config) {
2338
2545
  // src/api/activity.ts
2339
2546
  import { Router as Router5 } from "express";
2340
2547
  import { z as z5 } from "zod";
2341
- var logger3 = createLogger("api/activity");
2548
+ var logger6 = createLogger("api/activity");
2342
2549
  var usageSummaryQuerySchema = z5.object({
2343
2550
  months: z5.coerce.number().int().min(1).max(24).default(6)
2344
2551
  });
@@ -2430,7 +2637,7 @@ function createActivityRouter() {
2430
2637
  };
2431
2638
  return res.json(response);
2432
2639
  } catch (error) {
2433
- logger3.error("usage-summary error", error);
2640
+ logger6.error("usage-summary error", error);
2434
2641
  return res.status(500).json({
2435
2642
  error: "Internal Server Error",
2436
2643
  message: "Failed to compute usage summary"
@@ -2466,6 +2673,28 @@ function createActivityRouter() {
2466
2673
  startDate,
2467
2674
  endDate
2468
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
+ }
2469
2698
  const where = {
2470
2699
  tenantId,
2471
2700
  ...toolName && { toolName },
@@ -2505,10 +2734,10 @@ function createActivityRouter() {
2505
2734
  prisma2.toolExecutionLog.count({ where })
2506
2735
  ]);
2507
2736
  const clientIds = /* @__PURE__ */ new Set();
2508
- logs.forEach((log3) => {
2737
+ for (const log3 of logs) {
2509
2738
  if (log3.oauthClientId) clientIds.add(log3.oauthClientId);
2510
2739
  if (log3.apiKeyId) clientIds.add(log3.apiKeyId);
2511
- });
2740
+ }
2512
2741
  const [oauthClients, apiKeys] = await Promise.all([
2513
2742
  clientIds.size > 0 && logs.some((log3) => log3.oauthClientId) ? prisma2.oAuthClient.findMany({
2514
2743
  where: {
@@ -2525,13 +2754,13 @@ function createActivityRouter() {
2525
2754
  }) : Promise.resolve([])
2526
2755
  ]);
2527
2756
  const clientNameMap = /* @__PURE__ */ new Map();
2528
- oauthClients.forEach((client) => {
2757
+ for (const client of oauthClients) {
2529
2758
  clientNameMap.set(client.id, client.clientName);
2530
- });
2531
- apiKeys.forEach((key) => {
2759
+ }
2760
+ for (const key of apiKeys) {
2532
2761
  clientNameMap.set(key.id, key.name);
2533
- });
2534
- const responseLogs = logs.filter((log3) => log3.createdAt && !isNaN(new Date(log3.createdAt).getTime())).map((log3) => ({
2762
+ }
2763
+ const responseLogs = logs.filter((log3) => log3.createdAt && !Number.isNaN(new Date(log3.createdAt).getTime())).map((log3) => ({
2535
2764
  id: log3.id,
2536
2765
  toolName: log3.toolName,
2537
2766
  toolModule: log3.toolModule ?? void 0,
@@ -2553,7 +2782,7 @@ function createActivityRouter() {
2553
2782
  };
2554
2783
  return res.json(response);
2555
2784
  } catch (error) {
2556
- logger3.error("logs endpoint error", error, {
2785
+ logger6.error("logs endpoint error", error, {
2557
2786
  correlationId: req.headers["x-correlation-id"]
2558
2787
  });
2559
2788
  return res.status(500).json({
@@ -2562,6 +2791,107 @@ function createActivityRouter() {
2562
2791
  });
2563
2792
  }
2564
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
+ });
2565
2895
  router.get("/stats", async (req, res) => {
2566
2896
  try {
2567
2897
  const tenantId = req.tenantContext?.tenantId;
@@ -2588,7 +2918,30 @@ function createActivityRouter() {
2588
2918
  tenantId,
2589
2919
  createdAt: { gte: startDate }
2590
2920
  };
2591
- const [totalCalls, successCount, avgDuration, topTools, byClientGroup, byShopGroup] = await Promise.all([
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([
2592
2945
  // Total calls
2593
2946
  prisma2.toolExecutionLog.count({ where: baseWhere }),
2594
2947
  // Success count
@@ -2617,32 +2970,37 @@ function createActivityRouter() {
2617
2970
  where: baseWhere,
2618
2971
  _count: { id: true }
2619
2972
  }),
2620
- // By shop
2621
- prisma2.toolExecutionLog.groupBy({
2622
- by: ["shopDomain"],
2623
- where: {
2624
- ...baseWhere,
2625
- shopDomain: { not: null }
2626
- },
2627
- _count: { id: true }
2628
- })
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([])
2629
2987
  ]);
2630
- const byDayRaw = await prisma2.$queryRaw`
2631
- SELECT
2632
- date_trunc('day', created_at)::date AS date,
2633
- COUNT(*)::bigint AS calls,
2634
- COUNT(*) FILTER (WHERE status != 'SUCCESS')::bigint AS errors
2635
- FROM tool_execution_logs
2636
- WHERE tenant_id = ${tenantId}
2637
- AND created_at >= ${startDate}
2638
- GROUP BY 1
2639
- ORDER BY 1 ASC
2640
- `;
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
+ ` : [];
2641
2999
  const clientIds = /* @__PURE__ */ new Set();
2642
- byClientGroup.forEach((group) => {
3000
+ for (const group of byClientGroup) {
2643
3001
  if (group.oauthClientId) clientIds.add(group.oauthClientId);
2644
3002
  if (group.apiKeyId) clientIds.add(group.apiKeyId);
2645
- });
3003
+ }
2646
3004
  const [oauthClients, apiKeys] = await Promise.all([
2647
3005
  clientIds.size > 0 && byClientGroup.some((g) => g.oauthClientId) ? prisma2.oAuthClient.findMany({
2648
3006
  where: {
@@ -2659,12 +3017,12 @@ function createActivityRouter() {
2659
3017
  }) : Promise.resolve([])
2660
3018
  ]);
2661
3019
  const clientNameMap = /* @__PURE__ */ new Map();
2662
- oauthClients.forEach((client) => {
3020
+ for (const client of oauthClients) {
2663
3021
  clientNameMap.set(client.id, client.clientName);
2664
- });
2665
- apiKeys.forEach((key) => {
3022
+ }
3023
+ for (const key of apiKeys) {
2666
3024
  clientNameMap.set(key.id, key.name);
2667
- });
3025
+ }
2668
3026
  const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
2669
3027
  const avgDurationMs = avgDuration._avg.durationMs ?? 0;
2670
3028
  const topToolsFormatted = topTools.map((tool) => ({
@@ -2679,11 +3037,20 @@ function createActivityRouter() {
2679
3037
  type: group.clientType === "oauth_client" ? "oauth" : "api_key",
2680
3038
  calls: group._count.id
2681
3039
  }));
2682
- const byShopFormatted = byShopGroup.filter((group) => group.shopDomain).map((group) => ({
2683
- shopDomain: group.shopDomain,
2684
- calls: group._count.id
2685
- }));
2686
- const byDayFormatted = byDayRaw.filter((row) => row.date && !isNaN(new Date(row.date).getTime())).map((row) => ({
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) => ({
2687
3054
  date: new Date(row.date).toISOString().split("T")[0],
2688
3055
  // YYYY-MM-DD
2689
3056
  calls: Number(row.calls),
@@ -2702,7 +3069,7 @@ function createActivityRouter() {
2702
3069
  };
2703
3070
  return res.json(response);
2704
3071
  } catch (error) {
2705
- logger3.error("stats endpoint error", error, {
3072
+ logger6.error("stats endpoint error", error, {
2706
3073
  correlationId: req.headers["x-correlation-id"]
2707
3074
  });
2708
3075
  return res.status(500).json({
@@ -2716,7 +3083,7 @@ function createActivityRouter() {
2716
3083
 
2717
3084
  // src/api/oauth-clients.ts
2718
3085
  import { Router as Router6 } from "express";
2719
- var logger4 = createLogger("api/oauth-clients");
3086
+ var logger7 = createLogger("api/oauth-clients");
2720
3087
  var OAUTH_CLIENT_REVOKE_ACTION = "oauth_client:revoke";
2721
3088
  function createOAuthClientsRouter(options) {
2722
3089
  const { auditLogger: auditLogger2 } = options;
@@ -2734,13 +3101,163 @@ function createOAuthClientsRouter(options) {
2734
3101
  const clients = await getAuthorizedClients(prisma2, tenantId);
2735
3102
  return res.json({ clients });
2736
3103
  } catch (error) {
2737
- 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
+ });
2738
3107
  return res.status(500).json({
2739
3108
  error: "Internal Server Error",
2740
3109
  message: "An unexpected error occurred while listing OAuth clients"
2741
3110
  });
2742
3111
  }
2743
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
+ });
2744
3261
  router.get("/:clientId/metrics", async (req, res) => {
2745
3262
  try {
2746
3263
  const tenantId = req.tenantContext?.tenantId;
@@ -2751,19 +3268,45 @@ function createOAuthClientsRouter(options) {
2751
3268
  });
2752
3269
  }
2753
3270
  const { clientId } = req.params;
2754
- const clientExists = await prisma2.oAuthRefreshToken.findFirst({
3271
+ const clientRecord = await prisma2.oAuthRefreshToken.findFirst({
2755
3272
  where: {
2756
3273
  clientId,
2757
3274
  tenantId,
2758
3275
  revokedAt: null
3276
+ },
3277
+ include: {
3278
+ client: {
3279
+ select: { id: true }
3280
+ }
2759
3281
  }
2760
3282
  });
2761
- if (!clientExists) {
3283
+ if (!clientRecord) {
2762
3284
  return res.status(404).json({
2763
3285
  error: "Not Found",
2764
3286
  message: "OAuth client not found or no active tokens"
2765
3287
  });
2766
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
+ }
2767
3310
  const now = /* @__PURE__ */ new Date();
2768
3311
  const sevenDaysAgo = new Date(now);
2769
3312
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
@@ -2771,7 +3314,7 @@ function createOAuthClientsRouter(options) {
2771
3314
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
2772
3315
  const baseWhere = {
2773
3316
  tenantId,
2774
- oauthClientId: clientId
3317
+ oauthClientId: oauthClientDbId
2775
3318
  };
2776
3319
  const queryStartTime = Date.now();
2777
3320
  const [calls7d, calls30d, totalCalls, successCount, avgDuration, topTools, lastUsed] = await Promise.all([
@@ -2838,7 +3381,7 @@ function createOAuthClientsRouter(options) {
2838
3381
  }));
2839
3382
  const queryDuration = Date.now() - queryStartTime;
2840
3383
  if (queryDuration > 200) {
2841
- logger4.warn("OAuth client metrics query exceeded 200ms", {
3384
+ logger7.warn("OAuth client metrics query exceeded 200ms", {
2842
3385
  clientId,
2843
3386
  tenantId,
2844
3387
  durationMs: queryDuration
@@ -2855,7 +3398,7 @@ function createOAuthClientsRouter(options) {
2855
3398
  };
2856
3399
  return res.json(response);
2857
3400
  } catch (error) {
2858
- logger4.error("metrics endpoint error", error, {
3401
+ logger7.error("metrics endpoint error", error, {
2859
3402
  correlationId: req.headers["x-correlation-id"]
2860
3403
  });
2861
3404
  return res.status(500).json({
@@ -2921,7 +3464,10 @@ function createOAuthClientsRouter(options) {
2921
3464
  });
2922
3465
  return res.status(204).send();
2923
3466
  } catch (error) {
2924
- 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
+ });
2925
3471
  return res.status(500).json({
2926
3472
  error: "Internal Server Error",
2927
3473
  message: "An unexpected error occurred during revocation"
@@ -3014,7 +3560,7 @@ var oauthClientsRouter = createOAuthClientsRouter({
3014
3560
  });
3015
3561
 
3016
3562
  // src/lifecycle/shutdown.ts
3017
- var logger5 = createLogger("lifecycle/shutdown");
3563
+ var logger8 = createLogger("lifecycle/shutdown");
3018
3564
  var ShutdownManager = class {
3019
3565
  /** Current shutdown state */
3020
3566
  _state = "running";
@@ -3144,20 +3690,20 @@ var ShutdownManager = class {
3144
3690
  */
3145
3691
  registerSignalHandlers() {
3146
3692
  process.on("SIGTERM", () => {
3147
- logger5.info("Received SIGTERM signal");
3693
+ logger8.info("Received SIGTERM signal");
3148
3694
  this.initiateShutdown().catch((error) => {
3149
- logger5.error("Shutdown failed", error instanceof Error ? error : void 0);
3695
+ logger8.error("Shutdown failed", error instanceof Error ? error : void 0);
3150
3696
  process.exit(1);
3151
3697
  });
3152
3698
  });
3153
3699
  process.on("SIGINT", () => {
3154
- logger5.info("Received SIGINT signal");
3700
+ logger8.info("Received SIGINT signal");
3155
3701
  this.initiateShutdown().catch((error) => {
3156
- logger5.error("Shutdown failed", error instanceof Error ? error : void 0);
3702
+ logger8.error("Shutdown failed", error instanceof Error ? error : void 0);
3157
3703
  process.exit(1);
3158
3704
  });
3159
3705
  });
3160
- logger5.debug("Signal handlers registered (SIGTERM, SIGINT)");
3706
+ logger8.debug("Signal handlers registered (SIGTERM, SIGINT)");
3161
3707
  }
3162
3708
  /**
3163
3709
  * Initiate graceful shutdown
@@ -3175,13 +3721,13 @@ var ShutdownManager = class {
3175
3721
  */
3176
3722
  async initiateShutdown() {
3177
3723
  if (this.shutdownInProgress) {
3178
- logger5.debug("Shutdown already in progress, ignoring duplicate call");
3724
+ logger8.debug("Shutdown already in progress, ignoring duplicate call");
3179
3725
  return;
3180
3726
  }
3181
3727
  this.shutdownInProgress = true;
3182
3728
  this._state = "draining";
3183
3729
  this.drainingStartedAt = /* @__PURE__ */ new Date();
3184
- logger5.info("Shutdown initiated", {
3730
+ logger8.info("Shutdown initiated", {
3185
3731
  drainTimeoutMs: this.drainTimeoutMs,
3186
3732
  activeConnections: this.getRemainingConnections()
3187
3733
  });
@@ -3197,11 +3743,11 @@ var ShutdownManager = class {
3197
3743
  }
3198
3744
  if (this.mcpSessionsCleanup) {
3199
3745
  try {
3200
- logger5.debug("Closing MCP sessions");
3746
+ logger8.debug("Closing MCP sessions");
3201
3747
  await this.withTimeout(this.mcpSessionsCleanup(), 5e3, "MCP sessions cleanup");
3202
- logger5.debug("MCP sessions closed");
3748
+ logger8.debug("MCP sessions closed");
3203
3749
  } catch (error) {
3204
- logger5.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);
3205
3751
  }
3206
3752
  }
3207
3753
  const summary = await this.runCleanupCallbacks();
@@ -3219,14 +3765,14 @@ var ShutdownManager = class {
3219
3765
  resolve();
3220
3766
  return;
3221
3767
  }
3222
- logger5.debug("Stopping HTTP server from accepting new connections");
3768
+ logger8.debug("Stopping HTTP server from accepting new connections");
3223
3769
  this.httpServer.close((error) => {
3224
3770
  if (error) {
3225
3771
  if (error.code !== "ERR_SERVER_NOT_RUNNING") {
3226
- logger5.warn("HTTP server close error", { error: error.message });
3772
+ logger8.warn("HTTP server close error", { error: error.message });
3227
3773
  }
3228
3774
  }
3229
- logger5.debug("HTTP server stopped accepting connections");
3775
+ logger8.debug("HTTP server stopped accepting connections");
3230
3776
  resolve();
3231
3777
  });
3232
3778
  });
@@ -3239,7 +3785,7 @@ var ShutdownManager = class {
3239
3785
  while (this.getRemainingConnections() > 0) {
3240
3786
  await new Promise((resolve) => setTimeout(resolve, pollInterval));
3241
3787
  }
3242
- logger5.debug("All connections drained");
3788
+ logger8.debug("All connections drained");
3243
3789
  }
3244
3790
  /**
3245
3791
  * Create drain timeout promise
@@ -3247,7 +3793,7 @@ var ShutdownManager = class {
3247
3793
  createDrainTimeout() {
3248
3794
  return new Promise((resolve) => {
3249
3795
  this.drainTimeoutTimer = setTimeout(() => {
3250
- logger5.warn("Drain timeout reached, proceeding with shutdown", {
3796
+ logger8.warn("Drain timeout reached, proceeding with shutdown", {
3251
3797
  remainingConnections: this.getRemainingConnections(),
3252
3798
  drainTimeoutMs: this.drainTimeoutMs
3253
3799
  });
@@ -3262,7 +3808,7 @@ var ShutdownManager = class {
3262
3808
  async runCleanupCallbacks() {
3263
3809
  const drainTimeMs = this.drainingStartedAt ? Date.now() - this.drainingStartedAt.getTime() : 0;
3264
3810
  let cleanupErrors = 0;
3265
- logger5.debug("Running cleanup callbacks", {
3811
+ logger8.debug("Running cleanup callbacks", {
3266
3812
  count: this.cleanupCallbacks.length
3267
3813
  });
3268
3814
  for (let i = 0; i < this.cleanupCallbacks.length; i++) {
@@ -3275,7 +3821,7 @@ var ShutdownManager = class {
3275
3821
  );
3276
3822
  } catch (error) {
3277
3823
  cleanupErrors++;
3278
- logger5.error(
3824
+ logger8.error(
3279
3825
  `Cleanup callback ${i + 1} failed`,
3280
3826
  error instanceof Error ? error : void 0
3281
3827
  );
@@ -3309,7 +3855,7 @@ var ShutdownManager = class {
3309
3855
  * Log final shutdown summary
3310
3856
  */
3311
3857
  logShutdownSummary(summary) {
3312
- logger5.info("Shutdown complete", {
3858
+ logger8.info("Shutdown complete", {
3313
3859
  drainTimeMs: summary.drainTimeMs,
3314
3860
  cleanupCallbacksRun: summary.cleanupCallbacksRun,
3315
3861
  cleanupErrors: summary.cleanupErrors,
@@ -5457,6 +6003,18 @@ var TokenRefreshService = class {
5457
6003
  } catch (error) {
5458
6004
  const errorMessage = error instanceof Error ? error.message : "Unknown error during refresh";
5459
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
+ }
5460
6018
  return {
5461
6019
  success: false,
5462
6020
  error: errorMessage,
@@ -6290,6 +6848,104 @@ function createGDPRWebhooksRouter(options) {
6290
6848
  return router;
6291
6849
  }
6292
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
+
6293
6949
  // src/transports/http.ts
6294
6950
  async function createHttpTransport(server) {
6295
6951
  const app = express();
@@ -6305,7 +6961,7 @@ async function createHttpTransport(server) {
6305
6961
  }
6306
6962
  app.use(sentryRequestMiddleware);
6307
6963
  if (isRemoteMode(config)) {
6308
- const { createCorsMiddleware } = await import("./security-44M6F2QU.js");
6964
+ const { createCorsMiddleware } = await import("./security-6CNKRY2G.js");
6309
6965
  const { getAllowedOrigins } = await import("./schema-SOWYIQIV.js");
6310
6966
  const allowedOrigins = getAllowedOrigins(config);
6311
6967
  app.use(
@@ -6316,8 +6972,19 @@ async function createHttpTransport(server) {
6316
6972
  );
6317
6973
  log.info(`CORS middleware enabled with ${allowedOrigins.length} allowed origin(s)`);
6318
6974
  app.use((req, res, next) => {
6319
- const isOAuthRelatedPage = req.path.startsWith("/oauth/authorize") || req.path.startsWith("/app/oauth/") || req.path.startsWith("/app/login") || req.path === "/app" || req.path === "/app/";
6320
- const frameAncestors = isOAuthRelatedPage ? "frame-ancestors 'self' https://claude.ai https://*.anthropic.com https://*.claude.ai" : "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
+ }
6321
6988
  res.setHeader(
6322
6989
  "Content-Security-Policy",
6323
6990
  [
@@ -6341,15 +7008,53 @@ async function createHttpTransport(server) {
6341
7008
  let clientPool = null;
6342
7009
  let auditLogger2 = null;
6343
7010
  let cryptoService = null;
7011
+ let keepAliveInterval = null;
6344
7012
  if (isRemoteMode(config)) {
6345
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
+ }
6346
7019
  const prisma2 = getPrismaClient();
6347
7020
  const encryptionKey = requireEncryptionKey(config);
6348
7021
  cryptoService = new CredentialEncryptionService(encryptionKey);
6349
7022
  auditLogger2 = new AuditLogger(prisma2);
7023
+ getToolExecutionLogger(prisma2);
7024
+ log.info("ToolExecutionLogger initialized for activity logging");
6350
7025
  apiKeyService = new ApiKeyService(prisma2);
6351
7026
  clientPool = new TenantClientPool(cryptoService, prisma2);
6352
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
+ }
6353
7058
  if (config.SHOPIFY_CLIENT_ID && config.SHOPIFY_CLIENT_SECRET) {
6354
7059
  const tokenRefreshService = initializeTokenRefreshService(prisma2, cryptoService, {
6355
7060
  clientId: config.SHOPIFY_CLIENT_ID,
@@ -6473,7 +7178,7 @@ async function createHttpTransport(server) {
6473
7178
  }
6474
7179
  if (apiKeyService) {
6475
7180
  try {
6476
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7181
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6477
7182
  const authResult = await validateMcpBearerToken(
6478
7183
  `Bearer ${bearerToken}`,
6479
7184
  apiKeyService,
@@ -6543,7 +7248,7 @@ async function createHttpTransport(server) {
6543
7248
  }
6544
7249
  if (sessionContext && methodRequiresShop && isRemote && apiKeyService && clientPool && prisma2 && (!sessionContext.requestContext?.client || !sessionContext.requestContext?.shopDomain)) {
6545
7250
  log.debug(`[mcp] Lazy loading context for existing session: ${sessionPrefix}...`);
6546
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7251
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6547
7252
  const authResult = await validateMcpBearerToken(
6548
7253
  req.headers.authorization,
6549
7254
  apiKeyService,
@@ -6562,7 +7267,12 @@ async function createHttpTransport(server) {
6562
7267
  authResult.tenant.defaultShop.domain,
6563
7268
  authResult.tenant.tenantId,
6564
7269
  authResult.tenant.apiKeyId || "",
6565
- authResult.tenant.allowedShops
7270
+ authResult.tenant.allowedShops,
7271
+ {
7272
+ oauthClientId: authResult.tenant.oauthClientId,
7273
+ oauthClientName: authResult.tenant.oauthClientName,
7274
+ correlationId
7275
+ }
6566
7276
  ),
6567
7277
  correlationId
6568
7278
  };
@@ -6647,7 +7357,7 @@ async function createHttpTransport(server) {
6647
7357
  let shopifyClient;
6648
7358
  let multiTenantContext;
6649
7359
  if (isRemote && apiKeyService && clientPool && prisma2) {
6650
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7360
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6651
7361
  const authResult = await validateMcpBearerToken(
6652
7362
  req.headers.authorization,
6653
7363
  apiKeyService,
@@ -6728,7 +7438,12 @@ async function createHttpTransport(server) {
6728
7438
  mcpTenantContext.defaultShop.domain,
6729
7439
  mcpTenantContext.tenantId,
6730
7440
  mcpTenantContext.apiKeyId || "",
6731
- mcpTenantContext.allowedShops
7441
+ mcpTenantContext.allowedShops,
7442
+ {
7443
+ oauthClientId: mcpTenantContext.oauthClientId,
7444
+ oauthClientName: mcpTenantContext.oauthClientName,
7445
+ correlationId
7446
+ }
6732
7447
  ),
6733
7448
  correlationId
6734
7449
  };
@@ -6930,7 +7645,7 @@ async function createHttpTransport(server) {
6930
7645
  let shopifyClient;
6931
7646
  let multiTenantContext;
6932
7647
  if (isRemote && apiKeyService && clientPool && prisma2) {
6933
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7648
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
6934
7649
  const authResult = await validateMcpBearerToken(
6935
7650
  req.headers.authorization,
6936
7651
  apiKeyService,
@@ -7095,7 +7810,7 @@ data: ${JSON.stringify({
7095
7810
  const isRemote = isRemoteMode(config);
7096
7811
  const prisma2 = isRemote ? getPrismaClient() : null;
7097
7812
  if (isRemote && apiKeyService && prisma2) {
7098
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7813
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
7099
7814
  const authResult = await validateMcpBearerToken(
7100
7815
  req.headers.authorization,
7101
7816
  apiKeyService,
@@ -7146,7 +7861,7 @@ data: ${JSON.stringify({
7146
7861
  return;
7147
7862
  }
7148
7863
  if (body.method === "tools/list") {
7149
- const { getRegisteredTools } = await import("./tools-BCI3Z2AW.js");
7864
+ const { getRegisteredTools } = await import("./tools-SVKPHJYW.js");
7150
7865
  const tools = getRegisteredTools();
7151
7866
  const response = {
7152
7867
  jsonrpc: "2.0",
@@ -7184,7 +7899,7 @@ data: ${JSON.stringify({
7184
7899
  return;
7185
7900
  }
7186
7901
  if (body.method === "tools/call" && isRemote && apiKeyService && clientPool && prisma2) {
7187
- const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
7902
+ const { validateMcpBearerToken } = await import("./mcp-auth-54BVOYFJ.js");
7188
7903
  const authResult = await validateMcpBearerToken(
7189
7904
  req.headers.authorization,
7190
7905
  apiKeyService,
@@ -7241,7 +7956,7 @@ data: ${JSON.stringify({
7241
7956
  ),
7242
7957
  correlationId
7243
7958
  };
7244
- const { getToolByName } = await import("./tools-BCI3Z2AW.js");
7959
+ const { getToolByName } = await import("./tools-SVKPHJYW.js");
7245
7960
  const params = body.params;
7246
7961
  const rawToolName = params?.name;
7247
7962
  const toolArgs = params?.arguments || {};
@@ -7365,27 +8080,7 @@ data: ${JSON.stringify({
7365
8080
  const queryString = Object.keys(req.query).length ? `?${new URLSearchParams(req.query).toString()}` : "";
7366
8081
  res.redirect(307, `/sse${queryString}`);
7367
8082
  });
7368
- app.get("/metrics", async (_req, res) => {
7369
- if (!isMetricsEnabled(config)) {
7370
- res.status(404).json({
7371
- error: "Not Found",
7372
- message: "Metrics endpoint is disabled"
7373
- });
7374
- return;
7375
- }
7376
- try {
7377
- const metrics = await getMetricsOutput();
7378
- res.setHeader("Content-Type", getMetricsContentType());
7379
- res.send(metrics);
7380
- } catch (error) {
7381
- const message = error instanceof Error ? error.message : "Failed to collect metrics";
7382
- log.error("Metrics collection failed", error instanceof Error ? error : void 0);
7383
- res.status(500).json({
7384
- error: "Internal Server Error",
7385
- message
7386
- });
7387
- }
7388
- });
8083
+ registerHealthEndpoints(app, config);
7389
8084
  let oauthStatus = null;
7390
8085
  if (isRemoteMode(config)) {
7391
8086
  const shopifyClientId = config.SHOPIFY_CLIENT_ID;
@@ -7399,66 +8094,6 @@ data: ${JSON.stringify({
7399
8094
  redirectUri: shopifyClientId && shopifyClientSecret ? `${appUrl}/oauth/callback` : void 0
7400
8095
  };
7401
8096
  }
7402
- app.get("/health", async (_req, res) => {
7403
- try {
7404
- let drainingInfo;
7405
- if (shutdownManager.isDraining()) {
7406
- const info = shutdownManager.getDrainingInfo();
7407
- if (info) {
7408
- drainingInfo = {
7409
- startedAt: info.startedAt,
7410
- remainingConnections: shutdownManager.getRemainingConnections(),
7411
- shutdownDeadline: info.shutdownDeadline
7412
- };
7413
- }
7414
- }
7415
- const health = await checkHealth({ drainingInfo });
7416
- const statusCode = health.status === "draining" ? 503 : 200;
7417
- log.debug(`Health check: ${health.status}`);
7418
- const healthResponse = {
7419
- ...health,
7420
- ...isRemoteMode(config) && oauthStatus ? { oauth: oauthStatus } : {}
7421
- };
7422
- res.status(statusCode).json(healthResponse);
7423
- } catch (error) {
7424
- const message = error instanceof Error ? error.message : "Health check failed unexpectedly";
7425
- log.warn(`Health check failed unexpectedly: ${message}`);
7426
- res.status(503).json({
7427
- status: "unhealthy",
7428
- version: getVersion(),
7429
- shopify: {
7430
- connected: false,
7431
- error: "Health check failed unexpectedly"
7432
- },
7433
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7434
- });
7435
- }
7436
- });
7437
- const faviconPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR42u3NQQkAAAgEsItnQutYzRQ+hMH+S0+dikAgEAgEAoFAIPgSLM8edFuULS3fAAAAAElFTkSuQmCC";
7438
- app.get("/favicon.ico", (_req, res) => {
7439
- const faviconBuffer = Buffer.from(faviconPngBase64, "base64");
7440
- res.setHeader("Content-Type", "image/png");
7441
- res.setHeader("Content-Length", faviconBuffer.length.toString());
7442
- res.setHeader("Cache-Control", "public, max-age=86400");
7443
- res.send(faviconBuffer);
7444
- });
7445
- app.get("/favicon.png", (_req, res) => {
7446
- const faviconBuffer = Buffer.from(faviconPngBase64, "base64");
7447
- res.setHeader("Content-Type", "image/png");
7448
- res.setHeader("Content-Length", faviconBuffer.length.toString());
7449
- res.setHeader("Cache-Control", "public, max-age=86400");
7450
- res.send(faviconBuffer);
7451
- });
7452
- app.get("/favicon.svg", (_req, res) => {
7453
- const svgFavicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
7454
- <rect width="32" height="32" rx="6" fill="#96BF48"/>
7455
- <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"/>
7456
- <path d="M12 8v-2a4 4 0 0 1 8 0v2" fill="none" stroke="#96BF48" stroke-width="2" stroke-linecap="round"/>
7457
- </svg>`;
7458
- res.setHeader("Content-Type", "image/svg+xml");
7459
- res.setHeader("Cache-Control", "public, max-age=86400");
7460
- res.send(svgFavicon);
7461
- });
7462
8097
  const oauthDiscoveryService = createOAuthDiscoveryService(discoveryBaseUrl);
7463
8098
  app.get("/.well-known/oauth-authorization-server", (_req, res) => {
7464
8099
  const metadata = oauthDiscoveryService.getAuthorizationServerMetadata();
@@ -8089,6 +8724,13 @@ data: ${JSON.stringify({
8089
8724
  log.debug("Disconnecting Prisma");
8090
8725
  await disconnectPrisma();
8091
8726
  });
8727
+ shutdownManager.onShutdown(() => {
8728
+ if (keepAliveInterval) {
8729
+ log.debug("Stopping database keep-alive");
8730
+ clearInterval(keepAliveInterval);
8731
+ keepAliveInterval = null;
8732
+ }
8733
+ });
8092
8734
  }
8093
8735
  return {
8094
8736
  app,
@@ -8097,6 +8739,21 @@ data: ${JSON.stringify({
8097
8739
  shutdownManager.setHttpServer(httpServer);
8098
8740
  shutdownManager.registerSignalHandlers();
8099
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
+ }
8100
8757
  return httpServer;
8101
8758
  }
8102
8759
  };