@anton.andrusenko/shopify-mcp-admin 2.2.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JU5IFCVJ.js → chunk-CJXPHNYT.js} +36 -1
- package/dist/chunk-CZJ7LSEO.js +251 -0
- package/dist/{chunk-PQKNBYJN.js → chunk-H36XQ6QK.js} +6 -3
- package/dist/{chunk-LMFNHULG.js → chunk-UMNIRP6T.js} +645 -56
- 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 +3 -3
- package/dist/dashboard/mcp-icon.svg +29 -31
- package/dist/index.js +1516 -518
- package/dist/{mcp-auth-F25V6FEY.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-HVUCP53D.js → tools-SVKPHJYW.js} +2 -2
- package/package.json +10 -1
- package/dist/chunk-5QMYOO4B.js +0 -146
- package/dist/dashboard/assets/index-BfNrQS4y.js +0 -120
- package/dist/dashboard/assets/index-BfNrQS4y.js.map +0 -1
- package/dist/dashboard/assets/index-HBHxyHsM.css +0 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getConfig,
|
|
3
3
|
log,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
redactTokens,
|
|
5
|
+
sanitizeErrorMessage,
|
|
6
|
+
sanitizeLogMessage,
|
|
7
|
+
sanitizeParams,
|
|
8
|
+
truncateOutput
|
|
9
|
+
} from "./chunk-CZJ7LSEO.js";
|
|
6
10
|
import {
|
|
7
11
|
getAuthMode,
|
|
8
12
|
getConfiguredRole,
|
|
@@ -1308,12 +1312,580 @@ var zodToJsonSchema = (schema, options) => {
|
|
|
1308
1312
|
return combined;
|
|
1309
1313
|
};
|
|
1310
1314
|
|
|
1315
|
+
// src/logging/correlation.ts
|
|
1316
|
+
import { randomUUID } from "crypto";
|
|
1317
|
+
|
|
1318
|
+
// src/types/context.ts
|
|
1319
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1320
|
+
function createSingleTenantContext(client, shopDomain) {
|
|
1321
|
+
return {
|
|
1322
|
+
client,
|
|
1323
|
+
shopDomain
|
|
1324
|
+
// userId and apiKeyId are undefined in single-tenant mode
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function createMultiTenantContext(client, shopDomain, tenantId, apiKeyId, allowedShops, options) {
|
|
1328
|
+
return {
|
|
1329
|
+
client,
|
|
1330
|
+
shopDomain,
|
|
1331
|
+
userId: tenantId,
|
|
1332
|
+
// Map tenantId to userId for compatibility
|
|
1333
|
+
apiKeyId,
|
|
1334
|
+
tenantId,
|
|
1335
|
+
allowedShops,
|
|
1336
|
+
correlationId: options?.correlationId,
|
|
1337
|
+
oauthClientId: options?.oauthClientId,
|
|
1338
|
+
oauthClientName: options?.oauthClientName
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
var requestContextStorage = new AsyncLocalStorage();
|
|
1342
|
+
var fallbackContextStorage = /* @__PURE__ */ new Map();
|
|
1343
|
+
function setFallbackContext(key, context) {
|
|
1344
|
+
fallbackContextStorage.set(key, context);
|
|
1345
|
+
}
|
|
1346
|
+
function clearFallbackContext(key) {
|
|
1347
|
+
fallbackContextStorage.delete(key);
|
|
1348
|
+
}
|
|
1349
|
+
var currentContextKey;
|
|
1350
|
+
function setCurrentContextKey(key) {
|
|
1351
|
+
currentContextKey = key;
|
|
1352
|
+
}
|
|
1353
|
+
function getRequestContext() {
|
|
1354
|
+
const asyncContext = requestContextStorage.getStore();
|
|
1355
|
+
if (asyncContext) {
|
|
1356
|
+
return asyncContext;
|
|
1357
|
+
}
|
|
1358
|
+
if (currentContextKey) {
|
|
1359
|
+
return fallbackContextStorage.get(currentContextKey);
|
|
1360
|
+
}
|
|
1361
|
+
return void 0;
|
|
1362
|
+
}
|
|
1363
|
+
function isMultiTenantContext(context) {
|
|
1364
|
+
return "tenantId" in context && typeof context.tenantId === "string";
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/logging/correlation.ts
|
|
1368
|
+
var CORRELATION_ID_HEADER = "X-Correlation-ID";
|
|
1369
|
+
var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1370
|
+
function generateCorrelationId() {
|
|
1371
|
+
return randomUUID();
|
|
1372
|
+
}
|
|
1373
|
+
function isValidCorrelationId(id) {
|
|
1374
|
+
return UUID_V4_REGEX.test(id);
|
|
1375
|
+
}
|
|
1376
|
+
function correlationMiddleware(req, res, next) {
|
|
1377
|
+
const headerValue = req.headers[CORRELATION_ID_HEADER.toLowerCase()];
|
|
1378
|
+
let correlationId;
|
|
1379
|
+
if (headerValue && isValidCorrelationId(headerValue)) {
|
|
1380
|
+
correlationId = headerValue;
|
|
1381
|
+
} else {
|
|
1382
|
+
correlationId = generateCorrelationId();
|
|
1383
|
+
}
|
|
1384
|
+
res.locals.correlationId = correlationId;
|
|
1385
|
+
if (typeof res.set === "function") {
|
|
1386
|
+
res.set(
|
|
1387
|
+
CORRELATION_ID_HEADER,
|
|
1388
|
+
correlationId
|
|
1389
|
+
);
|
|
1390
|
+
} else {
|
|
1391
|
+
res.setHeader(CORRELATION_ID_HEADER, correlationId);
|
|
1392
|
+
}
|
|
1393
|
+
next();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/logging/structured-logger.ts
|
|
1397
|
+
import {
|
|
1398
|
+
pino as createPino,
|
|
1399
|
+
destination as pinoDestination,
|
|
1400
|
+
transport as pinoTransport
|
|
1401
|
+
} from "pino";
|
|
1402
|
+
|
|
1403
|
+
// src/monitoring/sentry.ts
|
|
1404
|
+
import * as Sentry from "@sentry/node";
|
|
1405
|
+
function initSentry(options) {
|
|
1406
|
+
const { dsn, environment = "production", release } = options;
|
|
1407
|
+
if (!dsn) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
Sentry.init({
|
|
1411
|
+
dsn,
|
|
1412
|
+
environment,
|
|
1413
|
+
// Sample 10% of transactions for performance monitoring (AC-13.7.4)
|
|
1414
|
+
tracesSampleRate: 0.1,
|
|
1415
|
+
// Release tracking for deployment correlation
|
|
1416
|
+
release: release || "unknown",
|
|
1417
|
+
// Sanitize sensitive data before sending to Sentry
|
|
1418
|
+
beforeSend(event) {
|
|
1419
|
+
if (event.request?.headers) {
|
|
1420
|
+
const sensitiveHeaders = [
|
|
1421
|
+
"authorization",
|
|
1422
|
+
"x-api-key",
|
|
1423
|
+
"cookie",
|
|
1424
|
+
"x-shopify-access-token",
|
|
1425
|
+
"x-shopify-hmac-sha256"
|
|
1426
|
+
];
|
|
1427
|
+
for (const header of sensitiveHeaders) {
|
|
1428
|
+
delete event.request.headers[header];
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (event.request?.data) {
|
|
1432
|
+
const data = event.request.data;
|
|
1433
|
+
const sensitiveKeys = ["token", "password", "secret", "key", "access_token"];
|
|
1434
|
+
for (const key of Object.keys(data)) {
|
|
1435
|
+
if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
|
|
1436
|
+
data[key] = "[REDACTED]";
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (event.message) {
|
|
1441
|
+
event.message = sanitizeErrorMessage2(event.message);
|
|
1442
|
+
}
|
|
1443
|
+
if (event.exception?.values) {
|
|
1444
|
+
for (const exception of event.exception.values) {
|
|
1445
|
+
if (exception.value) {
|
|
1446
|
+
exception.value = sanitizeErrorMessage2(exception.value);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return event;
|
|
1451
|
+
},
|
|
1452
|
+
// Integrations
|
|
1453
|
+
integrations: [
|
|
1454
|
+
// Capture uncaught exceptions and unhandled rejections
|
|
1455
|
+
Sentry.onUncaughtExceptionIntegration(),
|
|
1456
|
+
Sentry.onUnhandledRejectionIntegration(),
|
|
1457
|
+
// Capture HTTP request data
|
|
1458
|
+
Sentry.requestDataIntegration(),
|
|
1459
|
+
// Deduplicate similar errors
|
|
1460
|
+
Sentry.dedupeIntegration()
|
|
1461
|
+
]
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
function sanitizeErrorMessage2(message) {
|
|
1465
|
+
const patterns = [
|
|
1466
|
+
/shpat_[a-zA-Z0-9]+/g,
|
|
1467
|
+
/shpua_[a-zA-Z0-9]+/g,
|
|
1468
|
+
/Bearer\s+[a-zA-Z0-9_-]+/g,
|
|
1469
|
+
/access_token[=:]\s*[a-zA-Z0-9_-]+/gi,
|
|
1470
|
+
/client_secret[=:]\s*[a-zA-Z0-9_-]+/gi,
|
|
1471
|
+
/sk_live_[a-zA-Z0-9_-]+/g
|
|
1472
|
+
];
|
|
1473
|
+
let sanitized = message;
|
|
1474
|
+
for (const pattern of patterns) {
|
|
1475
|
+
sanitized = sanitized.replace(pattern, "[REDACTED]");
|
|
1476
|
+
}
|
|
1477
|
+
return sanitized;
|
|
1478
|
+
}
|
|
1479
|
+
function sentryRequestMiddleware(req, res, next) {
|
|
1480
|
+
const context = getRequestContext();
|
|
1481
|
+
if (context) {
|
|
1482
|
+
if (context.correlationId) {
|
|
1483
|
+
Sentry.setTag("correlationId", context.correlationId);
|
|
1484
|
+
Sentry.setContext("request", {
|
|
1485
|
+
correlationId: context.correlationId
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
if (isMultiTenantContext(context) && context.tenantId) {
|
|
1489
|
+
Sentry.setTag("tenantId", context.tenantId);
|
|
1490
|
+
Sentry.setContext("tenant", {
|
|
1491
|
+
tenantId: context.tenantId
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (context.shopDomain) {
|
|
1495
|
+
Sentry.setTag("shopDomain", context.shopDomain);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
Sentry.setContext("http", {
|
|
1499
|
+
method: req.method,
|
|
1500
|
+
url: req.url,
|
|
1501
|
+
path: req.path,
|
|
1502
|
+
query: req.query,
|
|
1503
|
+
userAgent: req.get("user-agent"),
|
|
1504
|
+
ip: req.ip
|
|
1505
|
+
});
|
|
1506
|
+
Sentry.addBreadcrumb({
|
|
1507
|
+
category: "http",
|
|
1508
|
+
message: `${req.method} ${req.path}`,
|
|
1509
|
+
level: "info",
|
|
1510
|
+
data: {
|
|
1511
|
+
method: req.method,
|
|
1512
|
+
path: req.path
|
|
1513
|
+
// statusCode will be updated when response finishes (see below)
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
res.on("finish", () => {
|
|
1517
|
+
Sentry.addBreadcrumb({
|
|
1518
|
+
category: "http",
|
|
1519
|
+
message: `${req.method} ${req.path} - ${res.statusCode}`,
|
|
1520
|
+
level: res.statusCode >= 400 ? "error" : "info",
|
|
1521
|
+
data: {
|
|
1522
|
+
method: req.method,
|
|
1523
|
+
path: req.path,
|
|
1524
|
+
statusCode: res.statusCode
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
next();
|
|
1529
|
+
}
|
|
1530
|
+
function sentryErrorHandler(error, req, _res, next) {
|
|
1531
|
+
Sentry.captureException(error, {
|
|
1532
|
+
tags: {
|
|
1533
|
+
path: req.path,
|
|
1534
|
+
method: req.method
|
|
1535
|
+
},
|
|
1536
|
+
extra: {
|
|
1537
|
+
url: req.url,
|
|
1538
|
+
query: req.query,
|
|
1539
|
+
body: req.body
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
next(error);
|
|
1543
|
+
}
|
|
1544
|
+
function captureError(error, context) {
|
|
1545
|
+
const requestContext = getRequestContext();
|
|
1546
|
+
Sentry.withScope((scope) => {
|
|
1547
|
+
if (requestContext?.correlationId) {
|
|
1548
|
+
scope.setTag("correlationId", requestContext.correlationId);
|
|
1549
|
+
scope.setContext("request", {
|
|
1550
|
+
correlationId: requestContext.correlationId
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
if (requestContext && isMultiTenantContext(requestContext) && requestContext.tenantId) {
|
|
1554
|
+
scope.setTag("tenantId", requestContext.tenantId);
|
|
1555
|
+
scope.setContext("tenant", {
|
|
1556
|
+
tenantId: requestContext.tenantId
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
if (requestContext?.shopDomain) {
|
|
1560
|
+
scope.setTag("shopDomain", requestContext.shopDomain);
|
|
1561
|
+
}
|
|
1562
|
+
if (context) {
|
|
1563
|
+
scope.setContext("additional", context);
|
|
1564
|
+
}
|
|
1565
|
+
Sentry.captureException(error);
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/logging/structured-logger.ts
|
|
1570
|
+
function sanitizeText(text) {
|
|
1571
|
+
return redactTokens(text);
|
|
1572
|
+
}
|
|
1573
|
+
function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1574
|
+
if (typeof obj === "string") {
|
|
1575
|
+
return sanitizeText(obj);
|
|
1576
|
+
}
|
|
1577
|
+
if (obj === null || typeof obj !== "object") {
|
|
1578
|
+
return obj;
|
|
1579
|
+
}
|
|
1580
|
+
if (seen.has(obj)) {
|
|
1581
|
+
return "[Circular]";
|
|
1582
|
+
}
|
|
1583
|
+
seen.add(obj);
|
|
1584
|
+
if (Array.isArray(obj)) {
|
|
1585
|
+
return obj.map((item) => sanitizeObject(item, seen));
|
|
1586
|
+
}
|
|
1587
|
+
const result = {};
|
|
1588
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1589
|
+
result[key] = sanitizeObject(value, seen);
|
|
1590
|
+
}
|
|
1591
|
+
return result;
|
|
1592
|
+
}
|
|
1593
|
+
function getInstanceId() {
|
|
1594
|
+
return process.env.INSTANCE_ID || process.env.HOSTNAME || "unknown";
|
|
1595
|
+
}
|
|
1596
|
+
function getLogLevel() {
|
|
1597
|
+
const level = process.env.LOG_LEVEL?.toLowerCase();
|
|
1598
|
+
if (level === "debug" || level === "info" || level === "warn" || level === "error") {
|
|
1599
|
+
return level;
|
|
1600
|
+
}
|
|
1601
|
+
return "info";
|
|
1602
|
+
}
|
|
1603
|
+
function isDevelopment() {
|
|
1604
|
+
return process.env.NODE_ENV === "development";
|
|
1605
|
+
}
|
|
1606
|
+
function createTransport() {
|
|
1607
|
+
if (isDevelopment()) {
|
|
1608
|
+
try {
|
|
1609
|
+
return {
|
|
1610
|
+
target: "pino-pretty",
|
|
1611
|
+
options: {
|
|
1612
|
+
destination: 2,
|
|
1613
|
+
// stderr (fd 2)
|
|
1614
|
+
colorize: true,
|
|
1615
|
+
translateTime: "HH:MM:ss.l",
|
|
1616
|
+
ignore: "pid,hostname"
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
} catch {
|
|
1620
|
+
return void 0;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return void 0;
|
|
1624
|
+
}
|
|
1625
|
+
function createBasePinoLogger() {
|
|
1626
|
+
const level = getLogLevel();
|
|
1627
|
+
const transport = createTransport();
|
|
1628
|
+
const instanceId = getInstanceId();
|
|
1629
|
+
const options = {
|
|
1630
|
+
level,
|
|
1631
|
+
// ISO 8601 timestamp format (AC-13.1.3)
|
|
1632
|
+
timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`,
|
|
1633
|
+
// Format level as string instead of number
|
|
1634
|
+
formatters: {
|
|
1635
|
+
level: (label) => ({ level: label })
|
|
1636
|
+
},
|
|
1637
|
+
// Base bindings (can be overridden by child loggers)
|
|
1638
|
+
// Story 13.5: Add instance ID for multi-instance deployment identification
|
|
1639
|
+
base: {
|
|
1640
|
+
instanceId
|
|
1641
|
+
// AC-13.5.1: Instance ID for load balancer distribution verification
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
if (transport) {
|
|
1645
|
+
return createPino(options, pinoTransport(transport));
|
|
1646
|
+
}
|
|
1647
|
+
return createPino(options, pinoDestination({ dest: 2, sync: false }));
|
|
1648
|
+
}
|
|
1649
|
+
var basePinoLogger = createBasePinoLogger();
|
|
1650
|
+
function createLogger(module) {
|
|
1651
|
+
function getContextFields() {
|
|
1652
|
+
const context = getRequestContext();
|
|
1653
|
+
const fields = { module };
|
|
1654
|
+
if (context) {
|
|
1655
|
+
if (context.correlationId) {
|
|
1656
|
+
fields.correlationId = context.correlationId;
|
|
1657
|
+
}
|
|
1658
|
+
if (context.shopDomain) {
|
|
1659
|
+
fields.shopDomain = context.shopDomain;
|
|
1660
|
+
}
|
|
1661
|
+
if (isMultiTenantContext(context)) {
|
|
1662
|
+
fields.tenantId = context.tenantId;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return fields;
|
|
1666
|
+
}
|
|
1667
|
+
function mergeData(data) {
|
|
1668
|
+
const contextFields = getContextFields();
|
|
1669
|
+
if (data) {
|
|
1670
|
+
const sanitizedData = sanitizeObject(data);
|
|
1671
|
+
return { ...contextFields, ...sanitizedData };
|
|
1672
|
+
}
|
|
1673
|
+
return contextFields;
|
|
1674
|
+
}
|
|
1675
|
+
function formatError(error) {
|
|
1676
|
+
return {
|
|
1677
|
+
error: {
|
|
1678
|
+
name: error.name,
|
|
1679
|
+
message: sanitizeText(error.message),
|
|
1680
|
+
stack: error.stack ? sanitizeText(error.stack) : void 0
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
const logger2 = {
|
|
1685
|
+
debug: (msg, data) => {
|
|
1686
|
+
basePinoLogger.debug(mergeData(data), sanitizeText(msg));
|
|
1687
|
+
},
|
|
1688
|
+
info: (msg, data) => {
|
|
1689
|
+
basePinoLogger.info(mergeData(data), sanitizeText(msg));
|
|
1690
|
+
},
|
|
1691
|
+
warn: (msg, data) => {
|
|
1692
|
+
basePinoLogger.warn(mergeData(data), sanitizeText(msg));
|
|
1693
|
+
},
|
|
1694
|
+
error: (msg, error, data) => {
|
|
1695
|
+
const merged = mergeData(data);
|
|
1696
|
+
if (error) {
|
|
1697
|
+
Object.assign(merged, formatError(error));
|
|
1698
|
+
captureError(error, data);
|
|
1699
|
+
}
|
|
1700
|
+
basePinoLogger.error(merged, sanitizeText(msg));
|
|
1701
|
+
},
|
|
1702
|
+
child: (bindings) => {
|
|
1703
|
+
const childModule = bindings.module || module;
|
|
1704
|
+
const childLogger = createLogger(childModule);
|
|
1705
|
+
const originalMergeData = mergeData;
|
|
1706
|
+
const enhancedMergeData = (data) => {
|
|
1707
|
+
const base = originalMergeData(data);
|
|
1708
|
+
const sanitizedBindings = sanitizeObject(bindings);
|
|
1709
|
+
return { ...base, ...sanitizedBindings };
|
|
1710
|
+
};
|
|
1711
|
+
return {
|
|
1712
|
+
debug: (msg, data) => {
|
|
1713
|
+
basePinoLogger.debug(enhancedMergeData(data), sanitizeText(msg));
|
|
1714
|
+
},
|
|
1715
|
+
info: (msg, data) => {
|
|
1716
|
+
basePinoLogger.info(enhancedMergeData(data), sanitizeText(msg));
|
|
1717
|
+
},
|
|
1718
|
+
warn: (msg, data) => {
|
|
1719
|
+
basePinoLogger.warn(enhancedMergeData(data), sanitizeText(msg));
|
|
1720
|
+
},
|
|
1721
|
+
error: (msg, error, data) => {
|
|
1722
|
+
const merged = enhancedMergeData(data);
|
|
1723
|
+
if (error) {
|
|
1724
|
+
Object.assign(merged, formatError(error));
|
|
1725
|
+
}
|
|
1726
|
+
basePinoLogger.error(merged, sanitizeText(msg));
|
|
1727
|
+
},
|
|
1728
|
+
child: childLogger.child
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
return logger2;
|
|
1733
|
+
}
|
|
1734
|
+
function flushLogs() {
|
|
1735
|
+
basePinoLogger.flush();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/logging/tool-execution-logger.ts
|
|
1739
|
+
import { Prisma } from "@prisma/client";
|
|
1740
|
+
var logger = createLogger("logging/tool-execution-logger");
|
|
1741
|
+
var VALIDATION_ERROR_KEYWORDS = [
|
|
1742
|
+
"validation failed",
|
|
1743
|
+
"required",
|
|
1744
|
+
"must be",
|
|
1745
|
+
"expected",
|
|
1746
|
+
"cannot be empty",
|
|
1747
|
+
"is not allowed",
|
|
1748
|
+
"malformed",
|
|
1749
|
+
"parse error",
|
|
1750
|
+
"zod"
|
|
1751
|
+
];
|
|
1752
|
+
var AUTH_ERROR_KEYWORDS = [
|
|
1753
|
+
"unauthorized",
|
|
1754
|
+
"unauthenticated",
|
|
1755
|
+
"forbidden",
|
|
1756
|
+
"access denied",
|
|
1757
|
+
"not authorized",
|
|
1758
|
+
"authentication required",
|
|
1759
|
+
"authentication failed",
|
|
1760
|
+
"permission denied",
|
|
1761
|
+
"invalid api key",
|
|
1762
|
+
"invalid token",
|
|
1763
|
+
"token expired"
|
|
1764
|
+
];
|
|
1765
|
+
var TIMEOUT_KEYWORDS = ["timeout", "timed out", "deadline exceeded", "econnreset", "socket hang"];
|
|
1766
|
+
function classifyError(error) {
|
|
1767
|
+
let message;
|
|
1768
|
+
if (error instanceof Error) {
|
|
1769
|
+
message = error.message.toLowerCase();
|
|
1770
|
+
} else if (error !== null && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
1771
|
+
message = error.message.toLowerCase();
|
|
1772
|
+
} else {
|
|
1773
|
+
message = String(error).toLowerCase();
|
|
1774
|
+
}
|
|
1775
|
+
const errorName = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Object" : "Error";
|
|
1776
|
+
const isToolError = error !== null && typeof error === "object" && "name" in error && error.name === "ToolError";
|
|
1777
|
+
if (AUTH_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
|
|
1778
|
+
return {
|
|
1779
|
+
status: "AUTH_ERROR",
|
|
1780
|
+
errorCode: isToolError ? "TOOL_AUTH_ERROR" : "AUTH_ERROR"
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
if (VALIDATION_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
|
|
1784
|
+
return {
|
|
1785
|
+
status: "VALIDATION_ERROR",
|
|
1786
|
+
errorCode: isToolError ? "TOOL_VALIDATION_ERROR" : "VALIDATION_ERROR"
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
if (isToolError) {
|
|
1790
|
+
return { status: "ERROR", errorCode: "TOOL_ERROR" };
|
|
1791
|
+
}
|
|
1792
|
+
if (TIMEOUT_KEYWORDS.some((kw) => message.includes(kw))) {
|
|
1793
|
+
return { status: "TIMEOUT", errorCode: "TIMEOUT" };
|
|
1794
|
+
}
|
|
1795
|
+
if (message.includes("graphql") || errorName === "GraphqlQueryError") {
|
|
1796
|
+
return { status: "ERROR", errorCode: "GRAPHQL_ERROR" };
|
|
1797
|
+
}
|
|
1798
|
+
return { status: "ERROR", errorCode: "UNKNOWN_ERROR" };
|
|
1799
|
+
}
|
|
1800
|
+
var ToolExecutionLogger = class {
|
|
1801
|
+
prisma;
|
|
1802
|
+
/**
|
|
1803
|
+
* Create a new ToolExecutionLogger
|
|
1804
|
+
*
|
|
1805
|
+
* @param prisma - Prisma client for database access
|
|
1806
|
+
*/
|
|
1807
|
+
constructor(prisma) {
|
|
1808
|
+
this.prisma = prisma;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Log a tool execution (fire-and-forget)
|
|
1812
|
+
*
|
|
1813
|
+
* This method does NOT await the database insert. The insert runs
|
|
1814
|
+
* asynchronously and any errors are caught and logged, not thrown.
|
|
1815
|
+
*
|
|
1816
|
+
* AC-16.2.7: Fire-and-forget pattern with max 5ms overhead.
|
|
1817
|
+
*
|
|
1818
|
+
* @param entry - Tool execution entry to log
|
|
1819
|
+
*
|
|
1820
|
+
* @example
|
|
1821
|
+
* ```typescript
|
|
1822
|
+
* logger.logExecution({
|
|
1823
|
+
* tenantId: 'tenant-uuid',
|
|
1824
|
+
* toolName: 'create-product',
|
|
1825
|
+
* toolModule: 'products',
|
|
1826
|
+
* clientType: 'api_key',
|
|
1827
|
+
* apiKeyId: 'apikey-uuid',
|
|
1828
|
+
* inputParams: { title: 'New Product' },
|
|
1829
|
+
* output: { productId: '123' },
|
|
1830
|
+
* status: 'SUCCESS',
|
|
1831
|
+
* durationMs: 150,
|
|
1832
|
+
* });
|
|
1833
|
+
* // Returns immediately, database insert happens async
|
|
1834
|
+
* ```
|
|
1835
|
+
*/
|
|
1836
|
+
logExecution(entry) {
|
|
1837
|
+
if (!entry.tenantId) {
|
|
1838
|
+
logger.debug("Skipping tool execution log: no tenantId", { toolName: entry.toolName });
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const sanitizedInput = entry.inputParams ? sanitizeParams(entry.inputParams) : void 0;
|
|
1842
|
+
const sanitizedOutput = entry.output ? truncateOutput(entry.output) : void 0;
|
|
1843
|
+
const sanitizedErrorMessage = entry.errorMessage ? sanitizeErrorMessage(entry.errorMessage) : void 0;
|
|
1844
|
+
const logData = {
|
|
1845
|
+
tenant: { connect: { id: entry.tenantId } },
|
|
1846
|
+
toolName: entry.toolName,
|
|
1847
|
+
toolModule: entry.toolModule,
|
|
1848
|
+
sessionId: entry.sessionId,
|
|
1849
|
+
correlationId: entry.correlationId,
|
|
1850
|
+
clientType: entry.clientType,
|
|
1851
|
+
apiKey: entry.apiKeyId ? { connect: { id: entry.apiKeyId } } : void 0,
|
|
1852
|
+
oauthClientId: entry.oauthClientId,
|
|
1853
|
+
shop: entry.shopId ? { connect: { id: entry.shopId } } : void 0,
|
|
1854
|
+
shopDomain: entry.shopDomain,
|
|
1855
|
+
inputParams: sanitizedInput ?? Prisma.JsonNull,
|
|
1856
|
+
outputSummary: sanitizedOutput ?? Prisma.JsonNull,
|
|
1857
|
+
status: entry.status,
|
|
1858
|
+
errorCode: entry.errorCode,
|
|
1859
|
+
errorMessage: sanitizedErrorMessage,
|
|
1860
|
+
durationMs: entry.durationMs,
|
|
1861
|
+
ipAddress: entry.ipAddress,
|
|
1862
|
+
userAgent: entry.userAgent
|
|
1863
|
+
};
|
|
1864
|
+
this.prisma.toolExecutionLog.create({ data: logData }).catch((err) => {
|
|
1865
|
+
logger.error("Failed to log tool execution", err, {
|
|
1866
|
+
toolName: entry.toolName,
|
|
1867
|
+
tenantId: entry.tenantId?.substring(0, 8)
|
|
1868
|
+
});
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
var toolExecutionLoggerInstance = null;
|
|
1873
|
+
function getToolExecutionLogger(prisma) {
|
|
1874
|
+
if (!toolExecutionLoggerInstance) {
|
|
1875
|
+
if (!prisma) {
|
|
1876
|
+
throw new Error("ToolExecutionLogger not initialized. Call with Prisma client first.");
|
|
1877
|
+
}
|
|
1878
|
+
toolExecutionLoggerInstance = new ToolExecutionLogger(prisma);
|
|
1879
|
+
}
|
|
1880
|
+
return toolExecutionLoggerInstance;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1311
1883
|
// src/shopify/client.ts
|
|
1312
1884
|
import "@shopify/shopify-api/adapters/node";
|
|
1313
1885
|
import { shopifyApi } from "@shopify/shopify-api";
|
|
1314
1886
|
|
|
1315
1887
|
// src/utils/errors.ts
|
|
1316
|
-
var
|
|
1888
|
+
var sanitizeErrorMessage3 = sanitizeLogMessage;
|
|
1317
1889
|
var ToolError = class _ToolError extends Error {
|
|
1318
1890
|
/** AI-friendly suggestion for error recovery */
|
|
1319
1891
|
suggestion;
|
|
@@ -1343,8 +1915,8 @@ function extractErrorMessage(error) {
|
|
|
1343
1915
|
}
|
|
1344
1916
|
function createToolError(error, suggestion) {
|
|
1345
1917
|
const message = extractErrorMessage(error);
|
|
1346
|
-
const safeMessage =
|
|
1347
|
-
const safeSuggestion =
|
|
1918
|
+
const safeMessage = sanitizeErrorMessage3(message);
|
|
1919
|
+
const safeSuggestion = sanitizeErrorMessage3(suggestion);
|
|
1348
1920
|
return {
|
|
1349
1921
|
isError: true,
|
|
1350
1922
|
content: [
|
|
@@ -1385,8 +1957,7 @@ function createToolSuccess(data) {
|
|
|
1385
1957
|
type: "text",
|
|
1386
1958
|
text
|
|
1387
1959
|
}
|
|
1388
|
-
]
|
|
1389
|
-
structuredContent: data
|
|
1960
|
+
]
|
|
1390
1961
|
};
|
|
1391
1962
|
}
|
|
1392
1963
|
|
|
@@ -1787,52 +2358,6 @@ async function validateShopifyToken(shopDomain, accessToken) {
|
|
|
1787
2358
|
}
|
|
1788
2359
|
}
|
|
1789
2360
|
|
|
1790
|
-
// src/types/context.ts
|
|
1791
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
1792
|
-
function createSingleTenantContext(client, shopDomain) {
|
|
1793
|
-
return {
|
|
1794
|
-
client,
|
|
1795
|
-
shopDomain
|
|
1796
|
-
// userId and apiKeyId are undefined in single-tenant mode
|
|
1797
|
-
};
|
|
1798
|
-
}
|
|
1799
|
-
function createMultiTenantContext(client, shopDomain, tenantId, apiKeyId, allowedShops) {
|
|
1800
|
-
return {
|
|
1801
|
-
client,
|
|
1802
|
-
shopDomain,
|
|
1803
|
-
userId: tenantId,
|
|
1804
|
-
// Map tenantId to userId for compatibility
|
|
1805
|
-
apiKeyId,
|
|
1806
|
-
tenantId,
|
|
1807
|
-
allowedShops
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
var requestContextStorage = new AsyncLocalStorage();
|
|
1811
|
-
var fallbackContextStorage = /* @__PURE__ */ new Map();
|
|
1812
|
-
function setFallbackContext(key, context) {
|
|
1813
|
-
fallbackContextStorage.set(key, context);
|
|
1814
|
-
}
|
|
1815
|
-
function clearFallbackContext(key) {
|
|
1816
|
-
fallbackContextStorage.delete(key);
|
|
1817
|
-
}
|
|
1818
|
-
var currentContextKey;
|
|
1819
|
-
function setCurrentContextKey(key) {
|
|
1820
|
-
currentContextKey = key;
|
|
1821
|
-
}
|
|
1822
|
-
function getRequestContext() {
|
|
1823
|
-
const asyncContext = requestContextStorage.getStore();
|
|
1824
|
-
if (asyncContext) {
|
|
1825
|
-
return asyncContext;
|
|
1826
|
-
}
|
|
1827
|
-
if (currentContextKey) {
|
|
1828
|
-
return fallbackContextStorage.get(currentContextKey);
|
|
1829
|
-
}
|
|
1830
|
-
return void 0;
|
|
1831
|
-
}
|
|
1832
|
-
function isMultiTenantContext(context) {
|
|
1833
|
-
return "tenantId" in context && typeof context.tenantId === "string";
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
2361
|
// src/tools/registration.ts
|
|
1837
2362
|
var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
1838
2363
|
function deriveDefaultAnnotations(name) {
|
|
@@ -1888,18 +2413,73 @@ function validateInput(schema, input, toolName) {
|
|
|
1888
2413
|
}
|
|
1889
2414
|
return result.data;
|
|
1890
2415
|
}
|
|
2416
|
+
function logToolExecution(toolName, params, result, status, startTime, error) {
|
|
2417
|
+
let config;
|
|
2418
|
+
try {
|
|
2419
|
+
config = getConfig();
|
|
2420
|
+
} catch {
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
if (!isRemoteMode(config)) {
|
|
2424
|
+
log.debug(`Tool execution logging skipped in local mode: ${toolName}`);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const context = getRequestContext();
|
|
2428
|
+
if (!context || !isMultiTenantContext(context)) {
|
|
2429
|
+
log.debug(`Tool execution logging skipped: no multi-tenant context for ${toolName}`);
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
const endTime = performance.now();
|
|
2433
|
+
const durationMs = Math.round(endTime - startTime);
|
|
2434
|
+
const multiTenantContext = context;
|
|
2435
|
+
const hasOAuthClient = !!multiTenantContext.oauthClientId;
|
|
2436
|
+
const clientType = hasOAuthClient ? "oauth_client" : "api_key";
|
|
2437
|
+
let errorClassification;
|
|
2438
|
+
if (typeof status === "object") {
|
|
2439
|
+
errorClassification = status;
|
|
2440
|
+
} else if (error) {
|
|
2441
|
+
errorClassification = classifyError(error);
|
|
2442
|
+
}
|
|
2443
|
+
const entry = {
|
|
2444
|
+
tenantId: multiTenantContext.tenantId,
|
|
2445
|
+
toolName,
|
|
2446
|
+
correlationId: context.correlationId,
|
|
2447
|
+
clientType,
|
|
2448
|
+
apiKeyId: !hasOAuthClient ? multiTenantContext.apiKeyId : void 0,
|
|
2449
|
+
oauthClientId: hasOAuthClient ? multiTenantContext.oauthClientId : void 0,
|
|
2450
|
+
shopDomain: context.shopDomain,
|
|
2451
|
+
inputParams: params,
|
|
2452
|
+
output: result,
|
|
2453
|
+
status: errorClassification ? errorClassification.status : "SUCCESS",
|
|
2454
|
+
errorCode: errorClassification?.errorCode,
|
|
2455
|
+
errorMessage: error instanceof Error ? error.message : error ? String(error) : void 0,
|
|
2456
|
+
durationMs
|
|
2457
|
+
};
|
|
2458
|
+
try {
|
|
2459
|
+
const logger2 = getToolExecutionLogger();
|
|
2460
|
+
logger2.logExecution(entry);
|
|
2461
|
+
} catch (logError) {
|
|
2462
|
+
log.debug(
|
|
2463
|
+
`Tool execution logger not available: ${logError instanceof Error ? logError.message : "unknown"}`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
1891
2467
|
function wrapToolHandler(toolName, schema, handler) {
|
|
1892
2468
|
return async (params) => {
|
|
2469
|
+
const startTime = performance.now();
|
|
1893
2470
|
log.debug(`Tool invoked: ${toolName}`);
|
|
1894
2471
|
try {
|
|
1895
2472
|
const validatedParams = validateInput(schema, params, toolName);
|
|
1896
2473
|
const result = await handler(validatedParams);
|
|
1897
2474
|
log.debug(`Tool ${toolName} completed successfully`);
|
|
2475
|
+
logToolExecution(toolName, params, result, "SUCCESS", startTime);
|
|
1898
2476
|
return createToolSuccess(result);
|
|
1899
2477
|
} catch (error) {
|
|
1900
2478
|
log.error(
|
|
1901
2479
|
`Tool ${toolName} failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1902
2480
|
);
|
|
2481
|
+
const errorClass = classifyError(error);
|
|
2482
|
+
logToolExecution(toolName, params, null, errorClass, startTime, error);
|
|
1903
2483
|
let suggestion;
|
|
1904
2484
|
if (error instanceof ToolError) {
|
|
1905
2485
|
suggestion = error.suggestion;
|
|
@@ -2052,10 +2632,10 @@ function createHandlerWithContext(contextAwareHandler) {
|
|
|
2052
2632
|
}
|
|
2053
2633
|
if (isRemoteMode(config)) {
|
|
2054
2634
|
log.error(
|
|
2055
|
-
`[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This
|
|
2635
|
+
`[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This likely means no shop is connected or shop tokens need re-authentication.`
|
|
2056
2636
|
);
|
|
2057
2637
|
throw new Error(
|
|
2058
|
-
"
|
|
2638
|
+
"No connected Shopify store found. Please connect a store through the dashboard, or reconnect if your store was previously connected."
|
|
2059
2639
|
);
|
|
2060
2640
|
}
|
|
2061
2641
|
const client = await getShopifyClient();
|
|
@@ -13981,7 +14561,11 @@ export {
|
|
|
13981
14561
|
setCurrentContextKey,
|
|
13982
14562
|
getRequestContext,
|
|
13983
14563
|
isMultiTenantContext,
|
|
13984
|
-
|
|
14564
|
+
initSentry,
|
|
14565
|
+
sentryRequestMiddleware,
|
|
14566
|
+
sentryErrorHandler,
|
|
14567
|
+
captureError,
|
|
14568
|
+
sanitizeErrorMessage3 as sanitizeErrorMessage,
|
|
13985
14569
|
createShopifyClient,
|
|
13986
14570
|
getShopifyClient,
|
|
13987
14571
|
validateShopifyToken,
|
|
@@ -13994,6 +14578,11 @@ export {
|
|
|
13994
14578
|
getStorePolicies,
|
|
13995
14579
|
getStoreAlerts,
|
|
13996
14580
|
getStoreInfo,
|
|
14581
|
+
generateCorrelationId,
|
|
14582
|
+
correlationMiddleware,
|
|
14583
|
+
createLogger,
|
|
14584
|
+
flushLogs,
|
|
14585
|
+
getToolExecutionLogger,
|
|
13997
14586
|
deriveDefaultAnnotations,
|
|
13998
14587
|
validateToolName,
|
|
13999
14588
|
isValidToolName,
|