@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/{chunk-JU5IFCVJ.js → chunk-CJXPHNYT.js} +36 -1
- package/dist/chunk-CZJ7LSEO.js +251 -0
- package/dist/{chunk-EQUN4XCH.js → chunk-H36XQ6QK.js} +1 -1
- package/dist/{chunk-RBXQOPVF.js → chunk-UMNIRP6T.js} +16 -135
- package/dist/dashboard/assets/index-DVjSu1HI.js +130 -0
- package/dist/dashboard/assets/index-DVjSu1HI.js.map +1 -0
- package/dist/dashboard/assets/index-DlTP0Kre.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/index.js +842 -185
- package/dist/{mcp-auth-CWOWKID3.js → mcp-auth-54BVOYFJ.js} +2 -2
- package/dist/{security-44M6F2QU.js → security-6CNKRY2G.js} +4 -1
- package/dist/{store-JK2ZU6DR.js → store-5NJBYK45.js} +2 -2
- package/dist/{tools-BCI3Z2AW.js → tools-SVKPHJYW.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-5QMYOO4B.js +0 -146
- package/dist/dashboard/assets/index-ClITn1me.css +0 -1
- package/dist/dashboard/assets/index-Cvo1L2xM.js +0 -126
- package/dist/dashboard/assets/index-Cvo1L2xM.js.map +0 -1
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-
|
|
33
|
+
} from "./chunk-UMNIRP6T.js";
|
|
32
34
|
import {
|
|
33
35
|
disconnectPrisma,
|
|
34
36
|
getPrismaClient,
|
|
35
37
|
prisma,
|
|
36
|
-
sessionStore
|
|
37
|
-
|
|
38
|
+
sessionStore,
|
|
39
|
+
warmupDatabase
|
|
40
|
+
} from "./chunk-CJXPHNYT.js";
|
|
38
41
|
import {
|
|
39
42
|
createJsonRpcError
|
|
40
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-H36XQ6QK.js";
|
|
41
44
|
import {
|
|
42
45
|
getConfig,
|
|
43
46
|
log,
|
|
44
47
|
sanitizeLogMessage
|
|
45
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2757
|
+
for (const client of oauthClients) {
|
|
2529
2758
|
clientNameMap.set(client.id, client.clientName);
|
|
2530
|
-
}
|
|
2531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
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
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3020
|
+
for (const client of oauthClients) {
|
|
2663
3021
|
clientNameMap.set(client.id, client.clientName);
|
|
2664
|
-
}
|
|
2665
|
-
|
|
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 =
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3693
|
+
logger8.info("Received SIGTERM signal");
|
|
3148
3694
|
this.initiateShutdown().catch((error) => {
|
|
3149
|
-
|
|
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
|
-
|
|
3700
|
+
logger8.info("Received SIGINT signal");
|
|
3155
3701
|
this.initiateShutdown().catch((error) => {
|
|
3156
|
-
|
|
3702
|
+
logger8.error("Shutdown failed", error instanceof Error ? error : void 0);
|
|
3157
3703
|
process.exit(1);
|
|
3158
3704
|
});
|
|
3159
3705
|
});
|
|
3160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3746
|
+
logger8.debug("Closing MCP sessions");
|
|
3201
3747
|
await this.withTimeout(this.mcpSessionsCleanup(), 5e3, "MCP sessions cleanup");
|
|
3202
|
-
|
|
3748
|
+
logger8.debug("MCP sessions closed");
|
|
3203
3749
|
} catch (error) {
|
|
3204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3772
|
+
logger8.warn("HTTP server close error", { error: error.message });
|
|
3227
3773
|
}
|
|
3228
3774
|
}
|
|
3229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
6320
|
-
const
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
};
|