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