@anton.andrusenko/shopify-mcp-admin 2.2.1 → 2.3.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.
@@ -1308,12 +1308,704 @@ var zodToJsonSchema = (schema, options) => {
1308
1308
  return combined;
1309
1309
  };
1310
1310
 
1311
+ // src/logging/correlation.ts
1312
+ import { randomUUID } from "crypto";
1313
+
1314
+ // src/types/context.ts
1315
+ import { AsyncLocalStorage } from "async_hooks";
1316
+ function createSingleTenantContext(client, shopDomain) {
1317
+ return {
1318
+ client,
1319
+ shopDomain
1320
+ // userId and apiKeyId are undefined in single-tenant mode
1321
+ };
1322
+ }
1323
+ function createMultiTenantContext(client, shopDomain, tenantId, apiKeyId, allowedShops, options) {
1324
+ return {
1325
+ client,
1326
+ shopDomain,
1327
+ userId: tenantId,
1328
+ // Map tenantId to userId for compatibility
1329
+ apiKeyId,
1330
+ tenantId,
1331
+ allowedShops,
1332
+ correlationId: options?.correlationId,
1333
+ oauthClientId: options?.oauthClientId,
1334
+ oauthClientName: options?.oauthClientName
1335
+ };
1336
+ }
1337
+ var requestContextStorage = new AsyncLocalStorage();
1338
+ var fallbackContextStorage = /* @__PURE__ */ new Map();
1339
+ function setFallbackContext(key, context) {
1340
+ fallbackContextStorage.set(key, context);
1341
+ }
1342
+ function clearFallbackContext(key) {
1343
+ fallbackContextStorage.delete(key);
1344
+ }
1345
+ var currentContextKey;
1346
+ function setCurrentContextKey(key) {
1347
+ currentContextKey = key;
1348
+ }
1349
+ function getRequestContext() {
1350
+ const asyncContext = requestContextStorage.getStore();
1351
+ if (asyncContext) {
1352
+ return asyncContext;
1353
+ }
1354
+ if (currentContextKey) {
1355
+ return fallbackContextStorage.get(currentContextKey);
1356
+ }
1357
+ return void 0;
1358
+ }
1359
+ function isMultiTenantContext(context) {
1360
+ return "tenantId" in context && typeof context.tenantId === "string";
1361
+ }
1362
+
1363
+ // src/logging/correlation.ts
1364
+ var CORRELATION_ID_HEADER = "X-Correlation-ID";
1365
+ 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;
1366
+ function generateCorrelationId() {
1367
+ return randomUUID();
1368
+ }
1369
+ function isValidCorrelationId(id) {
1370
+ return UUID_V4_REGEX.test(id);
1371
+ }
1372
+ function correlationMiddleware(req, res, next) {
1373
+ const headerValue = req.headers[CORRELATION_ID_HEADER.toLowerCase()];
1374
+ let correlationId;
1375
+ if (headerValue && isValidCorrelationId(headerValue)) {
1376
+ correlationId = headerValue;
1377
+ } else {
1378
+ correlationId = generateCorrelationId();
1379
+ }
1380
+ res.locals.correlationId = correlationId;
1381
+ if (typeof res.set === "function") {
1382
+ res.set(
1383
+ CORRELATION_ID_HEADER,
1384
+ correlationId
1385
+ );
1386
+ } else {
1387
+ res.setHeader(CORRELATION_ID_HEADER, correlationId);
1388
+ }
1389
+ next();
1390
+ }
1391
+
1392
+ // src/logging/structured-logger.ts
1393
+ import {
1394
+ pino as createPino,
1395
+ destination as pinoDestination,
1396
+ transport as pinoTransport
1397
+ } from "pino";
1398
+
1399
+ // src/monitoring/sentry.ts
1400
+ import * as Sentry from "@sentry/node";
1401
+ function initSentry(options) {
1402
+ const { dsn, environment = "production", release } = options;
1403
+ if (!dsn) {
1404
+ return;
1405
+ }
1406
+ Sentry.init({
1407
+ dsn,
1408
+ environment,
1409
+ // Sample 10% of transactions for performance monitoring (AC-13.7.4)
1410
+ tracesSampleRate: 0.1,
1411
+ // Release tracking for deployment correlation
1412
+ release: release || "unknown",
1413
+ // Sanitize sensitive data before sending to Sentry
1414
+ beforeSend(event) {
1415
+ if (event.request?.headers) {
1416
+ const sensitiveHeaders = [
1417
+ "authorization",
1418
+ "x-api-key",
1419
+ "cookie",
1420
+ "x-shopify-access-token",
1421
+ "x-shopify-hmac-sha256"
1422
+ ];
1423
+ for (const header of sensitiveHeaders) {
1424
+ delete event.request.headers[header];
1425
+ }
1426
+ }
1427
+ if (event.request?.data) {
1428
+ const data = event.request.data;
1429
+ const sensitiveKeys = ["token", "password", "secret", "key", "access_token"];
1430
+ for (const key of Object.keys(data)) {
1431
+ if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
1432
+ data[key] = "[REDACTED]";
1433
+ }
1434
+ }
1435
+ }
1436
+ if (event.message) {
1437
+ event.message = sanitizeErrorMessage(event.message);
1438
+ }
1439
+ if (event.exception?.values) {
1440
+ for (const exception of event.exception.values) {
1441
+ if (exception.value) {
1442
+ exception.value = sanitizeErrorMessage(exception.value);
1443
+ }
1444
+ }
1445
+ }
1446
+ return event;
1447
+ },
1448
+ // Integrations
1449
+ integrations: [
1450
+ // Capture uncaught exceptions and unhandled rejections
1451
+ Sentry.onUncaughtExceptionIntegration(),
1452
+ Sentry.onUnhandledRejectionIntegration(),
1453
+ // Capture HTTP request data
1454
+ Sentry.requestDataIntegration(),
1455
+ // Deduplicate similar errors
1456
+ Sentry.dedupeIntegration()
1457
+ ]
1458
+ });
1459
+ }
1460
+ function sanitizeErrorMessage(message) {
1461
+ const patterns = [
1462
+ /shpat_[a-zA-Z0-9]+/g,
1463
+ /shpua_[a-zA-Z0-9]+/g,
1464
+ /Bearer\s+[a-zA-Z0-9_-]+/g,
1465
+ /access_token[=:]\s*[a-zA-Z0-9_-]+/gi,
1466
+ /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi,
1467
+ /sk_live_[a-zA-Z0-9_-]+/g
1468
+ ];
1469
+ let sanitized = message;
1470
+ for (const pattern of patterns) {
1471
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
1472
+ }
1473
+ return sanitized;
1474
+ }
1475
+ function sentryRequestMiddleware(req, res, next) {
1476
+ const context = getRequestContext();
1477
+ if (context) {
1478
+ if (context.correlationId) {
1479
+ Sentry.setTag("correlationId", context.correlationId);
1480
+ Sentry.setContext("request", {
1481
+ correlationId: context.correlationId
1482
+ });
1483
+ }
1484
+ if (isMultiTenantContext(context) && context.tenantId) {
1485
+ Sentry.setTag("tenantId", context.tenantId);
1486
+ Sentry.setContext("tenant", {
1487
+ tenantId: context.tenantId
1488
+ });
1489
+ }
1490
+ if (context.shopDomain) {
1491
+ Sentry.setTag("shopDomain", context.shopDomain);
1492
+ }
1493
+ }
1494
+ Sentry.setContext("http", {
1495
+ method: req.method,
1496
+ url: req.url,
1497
+ path: req.path,
1498
+ query: req.query,
1499
+ userAgent: req.get("user-agent"),
1500
+ ip: req.ip
1501
+ });
1502
+ Sentry.addBreadcrumb({
1503
+ category: "http",
1504
+ message: `${req.method} ${req.path}`,
1505
+ level: "info",
1506
+ data: {
1507
+ method: req.method,
1508
+ path: req.path
1509
+ // statusCode will be updated when response finishes (see below)
1510
+ }
1511
+ });
1512
+ res.on("finish", () => {
1513
+ Sentry.addBreadcrumb({
1514
+ category: "http",
1515
+ message: `${req.method} ${req.path} - ${res.statusCode}`,
1516
+ level: res.statusCode >= 400 ? "error" : "info",
1517
+ data: {
1518
+ method: req.method,
1519
+ path: req.path,
1520
+ statusCode: res.statusCode
1521
+ }
1522
+ });
1523
+ });
1524
+ next();
1525
+ }
1526
+ function sentryErrorHandler(error, req, _res, next) {
1527
+ Sentry.captureException(error, {
1528
+ tags: {
1529
+ path: req.path,
1530
+ method: req.method
1531
+ },
1532
+ extra: {
1533
+ url: req.url,
1534
+ query: req.query,
1535
+ body: req.body
1536
+ }
1537
+ });
1538
+ next(error);
1539
+ }
1540
+ function captureError(error, context) {
1541
+ const requestContext = getRequestContext();
1542
+ Sentry.withScope((scope) => {
1543
+ if (requestContext?.correlationId) {
1544
+ scope.setTag("correlationId", requestContext.correlationId);
1545
+ scope.setContext("request", {
1546
+ correlationId: requestContext.correlationId
1547
+ });
1548
+ }
1549
+ if (requestContext && isMultiTenantContext(requestContext) && requestContext.tenantId) {
1550
+ scope.setTag("tenantId", requestContext.tenantId);
1551
+ scope.setContext("tenant", {
1552
+ tenantId: requestContext.tenantId
1553
+ });
1554
+ }
1555
+ if (requestContext?.shopDomain) {
1556
+ scope.setTag("shopDomain", requestContext.shopDomain);
1557
+ }
1558
+ if (context) {
1559
+ scope.setContext("additional", context);
1560
+ }
1561
+ Sentry.captureException(error);
1562
+ });
1563
+ }
1564
+
1565
+ // src/logging/structured-logger.ts
1566
+ var SANITIZATION_PATTERNS = [
1567
+ { pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
1568
+ { pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
1569
+ { pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
1570
+ { pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
1571
+ { pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" },
1572
+ { pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" }
1573
+ ];
1574
+ function sanitizeText(text) {
1575
+ let result = text;
1576
+ for (const { pattern, replacement } of SANITIZATION_PATTERNS) {
1577
+ result = result.replace(pattern, replacement);
1578
+ }
1579
+ return result;
1580
+ }
1581
+ function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
1582
+ if (typeof obj === "string") {
1583
+ return sanitizeText(obj);
1584
+ }
1585
+ if (obj === null || typeof obj !== "object") {
1586
+ return obj;
1587
+ }
1588
+ if (seen.has(obj)) {
1589
+ return "[Circular]";
1590
+ }
1591
+ seen.add(obj);
1592
+ if (Array.isArray(obj)) {
1593
+ return obj.map((item) => sanitizeObject(item, seen));
1594
+ }
1595
+ const result = {};
1596
+ for (const [key, value] of Object.entries(obj)) {
1597
+ result[key] = sanitizeObject(value, seen);
1598
+ }
1599
+ return result;
1600
+ }
1601
+ function getInstanceId() {
1602
+ return process.env.INSTANCE_ID || process.env.HOSTNAME || "unknown";
1603
+ }
1604
+ function getLogLevel() {
1605
+ const level = process.env.LOG_LEVEL?.toLowerCase();
1606
+ if (level === "debug" || level === "info" || level === "warn" || level === "error") {
1607
+ return level;
1608
+ }
1609
+ return "info";
1610
+ }
1611
+ function isDevelopment() {
1612
+ return process.env.NODE_ENV === "development";
1613
+ }
1614
+ function createTransport() {
1615
+ if (isDevelopment()) {
1616
+ try {
1617
+ return {
1618
+ target: "pino-pretty",
1619
+ options: {
1620
+ destination: 2,
1621
+ // stderr (fd 2)
1622
+ colorize: true,
1623
+ translateTime: "HH:MM:ss.l",
1624
+ ignore: "pid,hostname"
1625
+ }
1626
+ };
1627
+ } catch {
1628
+ return void 0;
1629
+ }
1630
+ }
1631
+ return void 0;
1632
+ }
1633
+ function createBasePinoLogger() {
1634
+ const level = getLogLevel();
1635
+ const transport = createTransport();
1636
+ const instanceId = getInstanceId();
1637
+ const options = {
1638
+ level,
1639
+ // ISO 8601 timestamp format (AC-13.1.3)
1640
+ timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`,
1641
+ // Format level as string instead of number
1642
+ formatters: {
1643
+ level: (label) => ({ level: label })
1644
+ },
1645
+ // Base bindings (can be overridden by child loggers)
1646
+ // Story 13.5: Add instance ID for multi-instance deployment identification
1647
+ base: {
1648
+ instanceId
1649
+ // AC-13.5.1: Instance ID for load balancer distribution verification
1650
+ }
1651
+ };
1652
+ if (transport) {
1653
+ return createPino(options, pinoTransport(transport));
1654
+ }
1655
+ return createPino(options, pinoDestination({ dest: 2, sync: false }));
1656
+ }
1657
+ var basePinoLogger = createBasePinoLogger();
1658
+ function createLogger(module) {
1659
+ function getContextFields() {
1660
+ const context = getRequestContext();
1661
+ const fields = { module };
1662
+ if (context) {
1663
+ if (context.correlationId) {
1664
+ fields.correlationId = context.correlationId;
1665
+ }
1666
+ if (context.shopDomain) {
1667
+ fields.shopDomain = context.shopDomain;
1668
+ }
1669
+ if (isMultiTenantContext(context)) {
1670
+ fields.tenantId = context.tenantId;
1671
+ }
1672
+ }
1673
+ return fields;
1674
+ }
1675
+ function mergeData(data) {
1676
+ const contextFields = getContextFields();
1677
+ if (data) {
1678
+ const sanitizedData = sanitizeObject(data);
1679
+ return { ...contextFields, ...sanitizedData };
1680
+ }
1681
+ return contextFields;
1682
+ }
1683
+ function formatError(error) {
1684
+ return {
1685
+ error: {
1686
+ name: error.name,
1687
+ message: sanitizeText(error.message),
1688
+ stack: error.stack ? sanitizeText(error.stack) : void 0
1689
+ }
1690
+ };
1691
+ }
1692
+ const logger2 = {
1693
+ debug: (msg, data) => {
1694
+ basePinoLogger.debug(mergeData(data), sanitizeText(msg));
1695
+ },
1696
+ info: (msg, data) => {
1697
+ basePinoLogger.info(mergeData(data), sanitizeText(msg));
1698
+ },
1699
+ warn: (msg, data) => {
1700
+ basePinoLogger.warn(mergeData(data), sanitizeText(msg));
1701
+ },
1702
+ error: (msg, error, data) => {
1703
+ const merged = mergeData(data);
1704
+ if (error) {
1705
+ Object.assign(merged, formatError(error));
1706
+ captureError(error, data);
1707
+ }
1708
+ basePinoLogger.error(merged, sanitizeText(msg));
1709
+ },
1710
+ child: (bindings) => {
1711
+ const childModule = bindings.module || module;
1712
+ const childLogger = createLogger(childModule);
1713
+ const originalMergeData = mergeData;
1714
+ const enhancedMergeData = (data) => {
1715
+ const base = originalMergeData(data);
1716
+ const sanitizedBindings = sanitizeObject(bindings);
1717
+ return { ...base, ...sanitizedBindings };
1718
+ };
1719
+ return {
1720
+ debug: (msg, data) => {
1721
+ basePinoLogger.debug(enhancedMergeData(data), sanitizeText(msg));
1722
+ },
1723
+ info: (msg, data) => {
1724
+ basePinoLogger.info(enhancedMergeData(data), sanitizeText(msg));
1725
+ },
1726
+ warn: (msg, data) => {
1727
+ basePinoLogger.warn(enhancedMergeData(data), sanitizeText(msg));
1728
+ },
1729
+ error: (msg, error, data) => {
1730
+ const merged = enhancedMergeData(data);
1731
+ if (error) {
1732
+ Object.assign(merged, formatError(error));
1733
+ }
1734
+ basePinoLogger.error(merged, sanitizeText(msg));
1735
+ },
1736
+ child: childLogger.child
1737
+ };
1738
+ }
1739
+ };
1740
+ return logger2;
1741
+ }
1742
+ function flushLogs() {
1743
+ basePinoLogger.flush();
1744
+ }
1745
+
1746
+ // src/logging/tool-execution-logger.ts
1747
+ import { Prisma } from "@prisma/client";
1748
+
1749
+ // src/logging/sanitize.ts
1750
+ var MAX_STRING_LENGTH = 1e3;
1751
+ var MAX_ARRAY_ITEMS = 10;
1752
+ var MAX_RECURSION_DEPTH = 5;
1753
+ var MAX_OUTPUT_BYTES = 5e3;
1754
+ var TOKEN_PATTERNS = [
1755
+ // Shopify access tokens (shpat_xxx, shpua_xxx) - include underscores in the pattern
1756
+ { pattern: /shpat_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
1757
+ { pattern: /shpua_[a-zA-Z0-9_]+/g, replacement: "[REDACTED]" },
1758
+ // Bearer tokens
1759
+ { pattern: /Bearer\s+[a-zA-Z0-9_.-]+/g, replacement: "Bearer [REDACTED]" },
1760
+ // OAuth access tokens (mcp_access_xxx)
1761
+ { pattern: /mcp_access_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1762
+ // Live/test API keys (sk_live_xxx, sk_test_xxx)
1763
+ { pattern: /sk_live_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1764
+ { pattern: /sk_test_[a-zA-Z0-9_-]+/g, replacement: "[REDACTED]" },
1765
+ // Generic access_token and client_secret patterns
1766
+ { pattern: /access_token[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "access_token=[REDACTED]" },
1767
+ { pattern: /client_secret[=:]\s*[a-zA-Z0-9_.-]+/gi, replacement: "client_secret=[REDACTED]" }
1768
+ ];
1769
+ function redactTokens(text) {
1770
+ let result = text;
1771
+ for (const { pattern, replacement } of TOKEN_PATTERNS) {
1772
+ result = result.replace(pattern, replacement);
1773
+ }
1774
+ return result;
1775
+ }
1776
+ function truncateString(text, maxLength = MAX_STRING_LENGTH) {
1777
+ if (text.length <= maxLength) {
1778
+ return text;
1779
+ }
1780
+ return `${text.substring(0, maxLength)}...[TRUNCATED]`;
1781
+ }
1782
+ function sanitizeParams(params, depth = 0) {
1783
+ if (depth > MAX_RECURSION_DEPTH) {
1784
+ return "[TRUNCATED]";
1785
+ }
1786
+ if (params === null || params === void 0) {
1787
+ return params;
1788
+ }
1789
+ if (typeof params === "string") {
1790
+ const redacted = redactTokens(params);
1791
+ return truncateString(redacted);
1792
+ }
1793
+ if (typeof params !== "object") {
1794
+ return params;
1795
+ }
1796
+ if (Array.isArray(params)) {
1797
+ const limited = params.slice(0, MAX_ARRAY_ITEMS);
1798
+ return limited.map((item) => sanitizeParams(item, depth + 1));
1799
+ }
1800
+ const result = {};
1801
+ for (const [key, value] of Object.entries(params)) {
1802
+ result[key] = sanitizeParams(value, depth + 1);
1803
+ }
1804
+ return result;
1805
+ }
1806
+ function getByteSize(value) {
1807
+ try {
1808
+ return JSON.stringify(value).length;
1809
+ } catch {
1810
+ return 0;
1811
+ }
1812
+ }
1813
+ function truncateOutput(output, maxBytes = MAX_OUTPUT_BYTES) {
1814
+ const sanitized = sanitizeParams(output);
1815
+ const size = getByteSize(sanitized);
1816
+ if (size <= maxBytes) {
1817
+ return sanitized;
1818
+ }
1819
+ if (typeof sanitized === "object" && sanitized !== null && !Array.isArray(sanitized)) {
1820
+ const obj = sanitized;
1821
+ if ("data" in obj || "errors" in obj) {
1822
+ const result = {};
1823
+ if (obj.errors) {
1824
+ result.errors = obj.errors;
1825
+ }
1826
+ if (obj.data) {
1827
+ result.data = "[TRUNCATED - output exceeded 5KB]";
1828
+ }
1829
+ if (obj.extensions && getByteSize(obj.extensions) < 200) {
1830
+ result.extensions = obj.extensions;
1831
+ }
1832
+ return result;
1833
+ }
1834
+ return {
1835
+ _truncated: true,
1836
+ _message: `Output truncated: ${size} bytes exceeded ${maxBytes} byte limit`,
1837
+ _keys: Object.keys(obj).slice(0, 10)
1838
+ };
1839
+ }
1840
+ if (Array.isArray(sanitized)) {
1841
+ return {
1842
+ _truncated: true,
1843
+ _message: `Array truncated: ${sanitized.length} items, ${size} bytes exceeded ${maxBytes} byte limit`,
1844
+ _sample: sanitized.slice(0, 3)
1845
+ };
1846
+ }
1847
+ return "[TRUNCATED - output exceeded 5KB]";
1848
+ }
1849
+ function sanitizeErrorMessage2(error) {
1850
+ if (error instanceof Error) {
1851
+ return truncateString(redactTokens(error.message), 500);
1852
+ }
1853
+ if (typeof error === "string") {
1854
+ return truncateString(redactTokens(error), 500);
1855
+ }
1856
+ return "Unknown error";
1857
+ }
1858
+
1859
+ // src/logging/tool-execution-logger.ts
1860
+ var logger = createLogger("logging/tool-execution-logger");
1861
+ var VALIDATION_ERROR_KEYWORDS = [
1862
+ "validation failed",
1863
+ "required",
1864
+ "must be",
1865
+ "expected",
1866
+ "cannot be empty",
1867
+ "is not allowed",
1868
+ "malformed",
1869
+ "parse error",
1870
+ "zod"
1871
+ ];
1872
+ var AUTH_ERROR_KEYWORDS = [
1873
+ "unauthorized",
1874
+ "unauthenticated",
1875
+ "forbidden",
1876
+ "access denied",
1877
+ "not authorized",
1878
+ "authentication required",
1879
+ "authentication failed",
1880
+ "permission denied",
1881
+ "invalid api key",
1882
+ "invalid token",
1883
+ "token expired"
1884
+ ];
1885
+ var TIMEOUT_KEYWORDS = ["timeout", "timed out", "deadline exceeded", "econnreset", "socket hang"];
1886
+ function classifyError(error) {
1887
+ let message;
1888
+ if (error instanceof Error) {
1889
+ message = error.message.toLowerCase();
1890
+ } else if (error !== null && typeof error === "object" && "message" in error && typeof error.message === "string") {
1891
+ message = error.message.toLowerCase();
1892
+ } else {
1893
+ message = String(error).toLowerCase();
1894
+ }
1895
+ const errorName = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Object" : "Error";
1896
+ const isToolError = error !== null && typeof error === "object" && "name" in error && error.name === "ToolError";
1897
+ if (AUTH_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
1898
+ return {
1899
+ status: "AUTH_ERROR",
1900
+ errorCode: isToolError ? "TOOL_AUTH_ERROR" : "AUTH_ERROR"
1901
+ };
1902
+ }
1903
+ if (VALIDATION_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
1904
+ return {
1905
+ status: "VALIDATION_ERROR",
1906
+ errorCode: isToolError ? "TOOL_VALIDATION_ERROR" : "VALIDATION_ERROR"
1907
+ };
1908
+ }
1909
+ if (isToolError) {
1910
+ return { status: "ERROR", errorCode: "TOOL_ERROR" };
1911
+ }
1912
+ if (TIMEOUT_KEYWORDS.some((kw) => message.includes(kw))) {
1913
+ return { status: "TIMEOUT", errorCode: "TIMEOUT" };
1914
+ }
1915
+ if (message.includes("graphql") || errorName === "GraphqlQueryError") {
1916
+ return { status: "ERROR", errorCode: "GRAPHQL_ERROR" };
1917
+ }
1918
+ return { status: "ERROR", errorCode: "UNKNOWN_ERROR" };
1919
+ }
1920
+ var ToolExecutionLogger = class {
1921
+ prisma;
1922
+ /**
1923
+ * Create a new ToolExecutionLogger
1924
+ *
1925
+ * @param prisma - Prisma client for database access
1926
+ */
1927
+ constructor(prisma) {
1928
+ this.prisma = prisma;
1929
+ }
1930
+ /**
1931
+ * Log a tool execution (fire-and-forget)
1932
+ *
1933
+ * This method does NOT await the database insert. The insert runs
1934
+ * asynchronously and any errors are caught and logged, not thrown.
1935
+ *
1936
+ * AC-16.2.7: Fire-and-forget pattern with max 5ms overhead.
1937
+ *
1938
+ * @param entry - Tool execution entry to log
1939
+ *
1940
+ * @example
1941
+ * ```typescript
1942
+ * logger.logExecution({
1943
+ * tenantId: 'tenant-uuid',
1944
+ * toolName: 'create-product',
1945
+ * toolModule: 'products',
1946
+ * clientType: 'api_key',
1947
+ * apiKeyId: 'apikey-uuid',
1948
+ * inputParams: { title: 'New Product' },
1949
+ * output: { productId: '123' },
1950
+ * status: 'SUCCESS',
1951
+ * durationMs: 150,
1952
+ * });
1953
+ * // Returns immediately, database insert happens async
1954
+ * ```
1955
+ */
1956
+ logExecution(entry) {
1957
+ if (!entry.tenantId) {
1958
+ logger.debug("Skipping tool execution log: no tenantId", { toolName: entry.toolName });
1959
+ return;
1960
+ }
1961
+ const sanitizedInput = entry.inputParams ? sanitizeParams(entry.inputParams) : void 0;
1962
+ const sanitizedOutput = entry.output ? truncateOutput(entry.output) : void 0;
1963
+ const sanitizedErrorMessage = entry.errorMessage ? sanitizeErrorMessage2(entry.errorMessage) : void 0;
1964
+ const logData = {
1965
+ tenant: { connect: { id: entry.tenantId } },
1966
+ toolName: entry.toolName,
1967
+ toolModule: entry.toolModule,
1968
+ sessionId: entry.sessionId,
1969
+ correlationId: entry.correlationId,
1970
+ clientType: entry.clientType,
1971
+ apiKey: entry.apiKeyId ? { connect: { id: entry.apiKeyId } } : void 0,
1972
+ oauthClientId: entry.oauthClientId,
1973
+ shop: entry.shopId ? { connect: { id: entry.shopId } } : void 0,
1974
+ shopDomain: entry.shopDomain,
1975
+ inputParams: sanitizedInput ?? Prisma.JsonNull,
1976
+ outputSummary: sanitizedOutput ?? Prisma.JsonNull,
1977
+ status: entry.status,
1978
+ errorCode: entry.errorCode,
1979
+ errorMessage: sanitizedErrorMessage,
1980
+ durationMs: entry.durationMs,
1981
+ ipAddress: entry.ipAddress,
1982
+ userAgent: entry.userAgent
1983
+ };
1984
+ this.prisma.toolExecutionLog.create({ data: logData }).catch((err) => {
1985
+ logger.error("Failed to log tool execution", err, {
1986
+ toolName: entry.toolName,
1987
+ tenantId: entry.tenantId?.substring(0, 8)
1988
+ });
1989
+ });
1990
+ }
1991
+ };
1992
+ var toolExecutionLoggerInstance = null;
1993
+ function getToolExecutionLogger(prisma) {
1994
+ if (!toolExecutionLoggerInstance) {
1995
+ if (!prisma) {
1996
+ throw new Error("ToolExecutionLogger not initialized. Call with Prisma client first.");
1997
+ }
1998
+ toolExecutionLoggerInstance = new ToolExecutionLogger(prisma);
1999
+ }
2000
+ return toolExecutionLoggerInstance;
2001
+ }
2002
+
1311
2003
  // src/shopify/client.ts
1312
2004
  import "@shopify/shopify-api/adapters/node";
1313
2005
  import { shopifyApi } from "@shopify/shopify-api";
1314
2006
 
1315
2007
  // src/utils/errors.ts
1316
- var sanitizeErrorMessage = sanitizeLogMessage;
2008
+ var sanitizeErrorMessage3 = sanitizeLogMessage;
1317
2009
  var ToolError = class _ToolError extends Error {
1318
2010
  /** AI-friendly suggestion for error recovery */
1319
2011
  suggestion;
@@ -1343,8 +2035,8 @@ function extractErrorMessage(error) {
1343
2035
  }
1344
2036
  function createToolError(error, suggestion) {
1345
2037
  const message = extractErrorMessage(error);
1346
- const safeMessage = sanitizeErrorMessage(message);
1347
- const safeSuggestion = sanitizeErrorMessage(suggestion);
2038
+ const safeMessage = sanitizeErrorMessage3(message);
2039
+ const safeSuggestion = sanitizeErrorMessage3(suggestion);
1348
2040
  return {
1349
2041
  isError: true,
1350
2042
  content: [
@@ -1787,52 +2479,6 @@ async function validateShopifyToken(shopDomain, accessToken) {
1787
2479
  }
1788
2480
  }
1789
2481
 
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
2482
  // src/tools/registration.ts
1837
2483
  var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
1838
2484
  function deriveDefaultAnnotations(name) {
@@ -1888,18 +2534,73 @@ function validateInput(schema, input, toolName) {
1888
2534
  }
1889
2535
  return result.data;
1890
2536
  }
2537
+ function logToolExecution(toolName, params, result, status, startTime, error) {
2538
+ let config;
2539
+ try {
2540
+ config = getConfig();
2541
+ } catch {
2542
+ return;
2543
+ }
2544
+ if (!isRemoteMode(config)) {
2545
+ log.debug(`Tool execution logging skipped in local mode: ${toolName}`);
2546
+ return;
2547
+ }
2548
+ const context = getRequestContext();
2549
+ if (!context || !isMultiTenantContext(context)) {
2550
+ log.debug(`Tool execution logging skipped: no multi-tenant context for ${toolName}`);
2551
+ return;
2552
+ }
2553
+ const endTime = performance.now();
2554
+ const durationMs = Math.round(endTime - startTime);
2555
+ const multiTenantContext = context;
2556
+ const hasOAuthClient = !!multiTenantContext.oauthClientId;
2557
+ const clientType = hasOAuthClient ? "oauth_client" : "api_key";
2558
+ let errorClassification;
2559
+ if (typeof status === "object") {
2560
+ errorClassification = status;
2561
+ } else if (error) {
2562
+ errorClassification = classifyError(error);
2563
+ }
2564
+ const entry = {
2565
+ tenantId: multiTenantContext.tenantId,
2566
+ toolName,
2567
+ correlationId: context.correlationId,
2568
+ clientType,
2569
+ apiKeyId: !hasOAuthClient ? multiTenantContext.apiKeyId : void 0,
2570
+ oauthClientId: hasOAuthClient ? multiTenantContext.oauthClientId : void 0,
2571
+ shopDomain: context.shopDomain,
2572
+ inputParams: params,
2573
+ output: result,
2574
+ status: errorClassification ? errorClassification.status : "SUCCESS",
2575
+ errorCode: errorClassification?.errorCode,
2576
+ errorMessage: error instanceof Error ? error.message : error ? String(error) : void 0,
2577
+ durationMs
2578
+ };
2579
+ try {
2580
+ const logger2 = getToolExecutionLogger();
2581
+ logger2.logExecution(entry);
2582
+ } catch (logError) {
2583
+ log.debug(
2584
+ `Tool execution logger not available: ${logError instanceof Error ? logError.message : "unknown"}`
2585
+ );
2586
+ }
2587
+ }
1891
2588
  function wrapToolHandler(toolName, schema, handler) {
1892
2589
  return async (params) => {
2590
+ const startTime = performance.now();
1893
2591
  log.debug(`Tool invoked: ${toolName}`);
1894
2592
  try {
1895
2593
  const validatedParams = validateInput(schema, params, toolName);
1896
2594
  const result = await handler(validatedParams);
1897
2595
  log.debug(`Tool ${toolName} completed successfully`);
2596
+ logToolExecution(toolName, params, result, "SUCCESS", startTime);
1898
2597
  return createToolSuccess(result);
1899
2598
  } catch (error) {
1900
2599
  log.error(
1901
2600
  `Tool ${toolName} failed: ${error instanceof Error ? error.message : "Unknown error"}`
1902
2601
  );
2602
+ const errorClass = classifyError(error);
2603
+ logToolExecution(toolName, params, null, errorClass, startTime, error);
1903
2604
  let suggestion;
1904
2605
  if (error instanceof ToolError) {
1905
2606
  suggestion = error.suggestion;
@@ -13981,7 +14682,10 @@ export {
13981
14682
  setCurrentContextKey,
13982
14683
  getRequestContext,
13983
14684
  isMultiTenantContext,
13984
- sanitizeErrorMessage,
14685
+ initSentry,
14686
+ sentryRequestMiddleware,
14687
+ sentryErrorHandler,
14688
+ sanitizeErrorMessage3 as sanitizeErrorMessage,
13985
14689
  createShopifyClient,
13986
14690
  getShopifyClient,
13987
14691
  validateShopifyToken,
@@ -13994,6 +14698,10 @@ export {
13994
14698
  getStorePolicies,
13995
14699
  getStoreAlerts,
13996
14700
  getStoreInfo,
14701
+ generateCorrelationId,
14702
+ correlationMiddleware,
14703
+ createLogger,
14704
+ flushLogs,
13997
14705
  deriveDefaultAnnotations,
13998
14706
  validateToolName,
13999
14707
  isValidToolName,