@anton.andrusenko/shopify-mcp-admin 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PQKNBYJN.js → chunk-EQUN4XCH.js} +5 -2
- package/dist/{chunk-LMFNHULG.js → chunk-RBXQOPVF.js} +758 -50
- package/dist/dashboard/assets/index-ClITn1me.css +1 -0
- package/dist/dashboard/assets/index-Cvo1L2xM.js +126 -0
- package/dist/dashboard/assets/index-Cvo1L2xM.js.map +1 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/dashboard/mcp-icon.svg +29 -31
- package/dist/index.js +781 -440
- package/dist/{mcp-auth-F25V6FEY.js → mcp-auth-CWOWKID3.js} +1 -1
- package/dist/{tools-HVUCP53D.js → tools-BCI3Z2AW.js} +1 -1
- package/package.json +10 -1
- 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,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
clearFallbackContext,
|
|
4
|
+
correlationMiddleware,
|
|
5
|
+
createLogger,
|
|
4
6
|
createMultiTenantContext,
|
|
5
7
|
createShopifyClient,
|
|
8
|
+
flushLogs,
|
|
9
|
+
generateCorrelationId,
|
|
6
10
|
getRequestContext,
|
|
7
11
|
getShopifyClient,
|
|
8
12
|
getStoreAlerts,
|
|
@@ -14,14 +18,17 @@ import {
|
|
|
14
18
|
getStorePolicies,
|
|
15
19
|
getStoreShipping,
|
|
16
20
|
getStoreTaxes,
|
|
21
|
+
initSentry,
|
|
17
22
|
isMultiTenantContext,
|
|
18
23
|
registerAllTools,
|
|
19
24
|
requestContextStorage,
|
|
20
25
|
sanitizeErrorMessage,
|
|
26
|
+
sentryErrorHandler,
|
|
27
|
+
sentryRequestMiddleware,
|
|
21
28
|
setCurrentContextKey,
|
|
22
29
|
setFallbackContext,
|
|
23
30
|
validateShopifyToken
|
|
24
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-RBXQOPVF.js";
|
|
25
32
|
import {
|
|
26
33
|
disconnectPrisma,
|
|
27
34
|
getPrismaClient,
|
|
@@ -30,7 +37,7 @@ import {
|
|
|
30
37
|
} from "./chunk-JU5IFCVJ.js";
|
|
31
38
|
import {
|
|
32
39
|
createJsonRpcError
|
|
33
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-EQUN4XCH.js";
|
|
34
41
|
import {
|
|
35
42
|
getConfig,
|
|
36
43
|
log,
|
|
@@ -49,172 +56,6 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
49
56
|
import { dirname as dirname2, join as join2 } from "path";
|
|
50
57
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
51
58
|
|
|
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
59
|
// src/server.ts
|
|
219
60
|
import { createRequire } from "module";
|
|
220
61
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -790,7 +631,7 @@ function createStdioTransport() {
|
|
|
790
631
|
}
|
|
791
632
|
|
|
792
633
|
// src/transports/http.ts
|
|
793
|
-
import { randomUUID as
|
|
634
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
794
635
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
795
636
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
796
637
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -805,194 +646,6 @@ import { z } from "zod";
|
|
|
805
646
|
|
|
806
647
|
// src/middleware/tenant-auth.ts
|
|
807
648
|
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
649
|
var logger = createLogger("middleware/tenant-auth");
|
|
997
650
|
async function requireTenantAuth(req, res, next) {
|
|
998
651
|
try {
|
|
@@ -2231,6 +1884,7 @@ function createShopsRouter(options) {
|
|
|
2231
1884
|
// src/api/keys.ts
|
|
2232
1885
|
import { Router as Router3 } from "express";
|
|
2233
1886
|
import { z as z3 } from "zod";
|
|
1887
|
+
var logger2 = createLogger("api/keys");
|
|
2234
1888
|
var DEFAULT_SCOPES = ["*"];
|
|
2235
1889
|
var createKeySchema = z3.object({
|
|
2236
1890
|
name: z3.string().min(1, "Name is required").max(100, "Name must be 100 characters or less"),
|
|
@@ -2329,6 +1983,114 @@ function createKeysRouter(options) {
|
|
|
2329
1983
|
});
|
|
2330
1984
|
}
|
|
2331
1985
|
});
|
|
1986
|
+
router.get("/:keyId/metrics", async (req, res) => {
|
|
1987
|
+
try {
|
|
1988
|
+
const tenantId = req.tenantContext?.tenantId;
|
|
1989
|
+
if (!tenantId) {
|
|
1990
|
+
return res.status(401).json({
|
|
1991
|
+
error: "Unauthorized",
|
|
1992
|
+
message: "Tenant context not found"
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
const { keyId } = req.params;
|
|
1996
|
+
const key = await prisma2.apiKey.findFirst({
|
|
1997
|
+
where: {
|
|
1998
|
+
id: keyId,
|
|
1999
|
+
tenantId,
|
|
2000
|
+
revokedAt: null
|
|
2001
|
+
},
|
|
2002
|
+
select: { id: true }
|
|
2003
|
+
});
|
|
2004
|
+
if (!key) {
|
|
2005
|
+
return res.status(404).json({
|
|
2006
|
+
error: "Not Found",
|
|
2007
|
+
message: "API key not found"
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
const now = /* @__PURE__ */ new Date();
|
|
2011
|
+
const sevenDaysAgo = new Date(now);
|
|
2012
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
2013
|
+
const thirtyDaysAgo = new Date(now);
|
|
2014
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
2015
|
+
const baseWhere = {
|
|
2016
|
+
tenantId,
|
|
2017
|
+
apiKeyId: keyId
|
|
2018
|
+
};
|
|
2019
|
+
const queryStartTime = Date.now();
|
|
2020
|
+
const [calls7d, calls30d, totalCalls, successCount, avgDuration, lastUsed] = await Promise.all([
|
|
2021
|
+
// Calls in last 7 days
|
|
2022
|
+
prisma2.toolExecutionLog.count({
|
|
2023
|
+
where: {
|
|
2024
|
+
...baseWhere,
|
|
2025
|
+
createdAt: { gte: sevenDaysAgo }
|
|
2026
|
+
}
|
|
2027
|
+
}),
|
|
2028
|
+
// Calls in last 30 days
|
|
2029
|
+
prisma2.toolExecutionLog.count({
|
|
2030
|
+
where: {
|
|
2031
|
+
...baseWhere,
|
|
2032
|
+
createdAt: { gte: thirtyDaysAgo }
|
|
2033
|
+
}
|
|
2034
|
+
}),
|
|
2035
|
+
// Total calls (for success rate calculation)
|
|
2036
|
+
prisma2.toolExecutionLog.count({
|
|
2037
|
+
where: {
|
|
2038
|
+
...baseWhere,
|
|
2039
|
+
createdAt: { gte: sevenDaysAgo }
|
|
2040
|
+
}
|
|
2041
|
+
}),
|
|
2042
|
+
// Success count (for success rate calculation)
|
|
2043
|
+
prisma2.toolExecutionLog.count({
|
|
2044
|
+
where: {
|
|
2045
|
+
...baseWhere,
|
|
2046
|
+
createdAt: { gte: sevenDaysAgo },
|
|
2047
|
+
status: "SUCCESS"
|
|
2048
|
+
}
|
|
2049
|
+
}),
|
|
2050
|
+
// Average duration
|
|
2051
|
+
prisma2.toolExecutionLog.aggregate({
|
|
2052
|
+
where: {
|
|
2053
|
+
...baseWhere,
|
|
2054
|
+
createdAt: { gte: sevenDaysAgo },
|
|
2055
|
+
durationMs: { not: null }
|
|
2056
|
+
},
|
|
2057
|
+
_avg: { durationMs: true }
|
|
2058
|
+
}),
|
|
2059
|
+
// Most recent execution
|
|
2060
|
+
prisma2.toolExecutionLog.findFirst({
|
|
2061
|
+
where: baseWhere,
|
|
2062
|
+
orderBy: { createdAt: "desc" },
|
|
2063
|
+
select: { createdAt: true }
|
|
2064
|
+
})
|
|
2065
|
+
]);
|
|
2066
|
+
const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
|
|
2067
|
+
const queryDuration = Date.now() - queryStartTime;
|
|
2068
|
+
if (queryDuration > 200) {
|
|
2069
|
+
logger2.warn("API key metrics query exceeded 200ms", {
|
|
2070
|
+
keyId,
|
|
2071
|
+
tenantId,
|
|
2072
|
+
durationMs: queryDuration
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
const response = {
|
|
2076
|
+
calls7d,
|
|
2077
|
+
calls30d,
|
|
2078
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
2079
|
+
// Round to 1 decimal place
|
|
2080
|
+
avgDurationMs: Math.round(avgDuration._avg.durationMs ?? 0),
|
|
2081
|
+
lastUsedAt: lastUsed?.createdAt.toISOString() ?? null
|
|
2082
|
+
};
|
|
2083
|
+
return res.json(response);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
logger2.error("metrics endpoint error", error, {
|
|
2086
|
+
correlationId: req.headers["x-correlation-id"]
|
|
2087
|
+
});
|
|
2088
|
+
return res.status(500).json({
|
|
2089
|
+
error: "Internal Server Error",
|
|
2090
|
+
message: "Failed to compute API key metrics"
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2332
2094
|
router.delete("/:keyId", async (req, res) => {
|
|
2333
2095
|
try {
|
|
2334
2096
|
const tenantId = req.tenantContext?.tenantId;
|
|
@@ -2467,13 +2229,14 @@ function createAuthRouter(_config) {
|
|
|
2467
2229
|
}
|
|
2468
2230
|
}
|
|
2469
2231
|
}
|
|
2232
|
+
const sameSiteSetting = isProduction ? "none" : "lax";
|
|
2470
2233
|
res.cookie("session_id", sessionId, {
|
|
2471
2234
|
httpOnly: true,
|
|
2472
2235
|
// Prevents JavaScript access (XSS protection)
|
|
2473
2236
|
secure: isProduction,
|
|
2474
|
-
// HTTPS only in production
|
|
2475
|
-
sameSite:
|
|
2476
|
-
//
|
|
2237
|
+
// HTTPS only in production (required for SameSite=None)
|
|
2238
|
+
sameSite: sameSiteSetting,
|
|
2239
|
+
// 'none' in production for Claude.ai OAuth iframe compatibility
|
|
2477
2240
|
path: "/",
|
|
2478
2241
|
// Explicitly set path
|
|
2479
2242
|
domain: cookieDomain,
|
|
@@ -2527,7 +2290,7 @@ function createAuthRouter(_config) {
|
|
|
2527
2290
|
res.clearCookie("session_id", {
|
|
2528
2291
|
httpOnly: true,
|
|
2529
2292
|
secure: isProduction,
|
|
2530
|
-
sameSite: "lax",
|
|
2293
|
+
sameSite: isProduction ? "none" : "lax",
|
|
2531
2294
|
path: "/",
|
|
2532
2295
|
domain: cookieDomain
|
|
2533
2296
|
});
|
|
@@ -2555,7 +2318,7 @@ function createAuthRouter(_config) {
|
|
|
2555
2318
|
res.clearCookie("session_id", {
|
|
2556
2319
|
httpOnly: true,
|
|
2557
2320
|
secure: isProduction,
|
|
2558
|
-
sameSite: "lax",
|
|
2321
|
+
sameSite: isProduction ? "none" : "lax",
|
|
2559
2322
|
path: "/",
|
|
2560
2323
|
domain: cookieDomain
|
|
2561
2324
|
});
|
|
@@ -2575,9 +2338,36 @@ function createAuthRouter(_config) {
|
|
|
2575
2338
|
// src/api/activity.ts
|
|
2576
2339
|
import { Router as Router5 } from "express";
|
|
2577
2340
|
import { z as z5 } from "zod";
|
|
2341
|
+
var logger3 = createLogger("api/activity");
|
|
2578
2342
|
var usageSummaryQuerySchema = z5.object({
|
|
2579
2343
|
months: z5.coerce.number().int().min(1).max(24).default(6)
|
|
2580
2344
|
});
|
|
2345
|
+
var activityLogsQuerySchema = z5.object({
|
|
2346
|
+
page: z5.coerce.number().int().min(1).default(1),
|
|
2347
|
+
limit: z5.coerce.number().int().min(1).transform((val) => Math.min(val, 100)).default(50),
|
|
2348
|
+
toolName: z5.string().optional(),
|
|
2349
|
+
clientType: z5.enum(["api_key", "oauth_client"]).optional(),
|
|
2350
|
+
oauthClientId: z5.string().uuid("Invalid OAuth client ID format").optional(),
|
|
2351
|
+
apiKeyId: z5.string().uuid("Invalid API key ID format").optional(),
|
|
2352
|
+
shopId: z5.string().uuid("Invalid shop ID format").optional(),
|
|
2353
|
+
status: z5.enum(["SUCCESS", "ERROR", "VALIDATION_ERROR", "AUTH_ERROR", "TIMEOUT"]).optional(),
|
|
2354
|
+
startDate: z5.string().datetime().optional(),
|
|
2355
|
+
endDate: z5.string().datetime().optional()
|
|
2356
|
+
}).refine(
|
|
2357
|
+
(data) => {
|
|
2358
|
+
if (data.startDate && data.endDate) {
|
|
2359
|
+
return new Date(data.endDate) >= new Date(data.startDate);
|
|
2360
|
+
}
|
|
2361
|
+
return true;
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
message: "endDate must be greater than or equal to startDate",
|
|
2365
|
+
path: ["endDate"]
|
|
2366
|
+
}
|
|
2367
|
+
);
|
|
2368
|
+
var activityStatsQuerySchema = z5.object({
|
|
2369
|
+
period: z5.enum(["7d", "30d", "90d"]).default("7d")
|
|
2370
|
+
});
|
|
2581
2371
|
function startOfMonth(d) {
|
|
2582
2372
|
const out = new Date(d);
|
|
2583
2373
|
out.setDate(1);
|
|
@@ -2640,18 +2430,293 @@ function createActivityRouter() {
|
|
|
2640
2430
|
};
|
|
2641
2431
|
return res.json(response);
|
|
2642
2432
|
} catch (error) {
|
|
2643
|
-
|
|
2433
|
+
logger3.error("usage-summary error", error);
|
|
2644
2434
|
return res.status(500).json({
|
|
2645
2435
|
error: "Internal Server Error",
|
|
2646
2436
|
message: "Failed to compute usage summary"
|
|
2647
2437
|
});
|
|
2648
2438
|
}
|
|
2649
2439
|
});
|
|
2440
|
+
router.get("/logs", async (req, res) => {
|
|
2441
|
+
try {
|
|
2442
|
+
const tenantId = req.tenantContext?.tenantId;
|
|
2443
|
+
if (!tenantId) {
|
|
2444
|
+
return res.status(401).json({
|
|
2445
|
+
error: "Unauthorized",
|
|
2446
|
+
message: "Tenant context not found"
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
const parsed = activityLogsQuerySchema.safeParse(req.query);
|
|
2450
|
+
if (!parsed.success) {
|
|
2451
|
+
return res.status(400).json({
|
|
2452
|
+
error: "Validation Error",
|
|
2453
|
+
message: "Invalid query parameters",
|
|
2454
|
+
details: parsed.error.errors
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
const {
|
|
2458
|
+
page,
|
|
2459
|
+
limit,
|
|
2460
|
+
toolName,
|
|
2461
|
+
clientType,
|
|
2462
|
+
oauthClientId,
|
|
2463
|
+
apiKeyId,
|
|
2464
|
+
shopId,
|
|
2465
|
+
status,
|
|
2466
|
+
startDate,
|
|
2467
|
+
endDate
|
|
2468
|
+
} = parsed.data;
|
|
2469
|
+
const where = {
|
|
2470
|
+
tenantId,
|
|
2471
|
+
...toolName && { toolName },
|
|
2472
|
+
...clientType && { clientType },
|
|
2473
|
+
...oauthClientId && { oauthClientId },
|
|
2474
|
+
...apiKeyId && { apiKeyId },
|
|
2475
|
+
...shopId && { shopId },
|
|
2476
|
+
...status && { status },
|
|
2477
|
+
...(startDate || endDate) && {
|
|
2478
|
+
createdAt: {
|
|
2479
|
+
...startDate && { gte: new Date(startDate) },
|
|
2480
|
+
...endDate && { lte: new Date(endDate) }
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
const skip = (page - 1) * limit;
|
|
2485
|
+
const take = limit;
|
|
2486
|
+
const [logs, total] = await Promise.all([
|
|
2487
|
+
prisma2.toolExecutionLog.findMany({
|
|
2488
|
+
where,
|
|
2489
|
+
skip,
|
|
2490
|
+
take,
|
|
2491
|
+
orderBy: { createdAt: "desc" },
|
|
2492
|
+
select: {
|
|
2493
|
+
id: true,
|
|
2494
|
+
toolName: true,
|
|
2495
|
+
toolModule: true,
|
|
2496
|
+
status: true,
|
|
2497
|
+
clientType: true,
|
|
2498
|
+
oauthClientId: true,
|
|
2499
|
+
apiKeyId: true,
|
|
2500
|
+
shopDomain: true,
|
|
2501
|
+
durationMs: true,
|
|
2502
|
+
createdAt: true
|
|
2503
|
+
}
|
|
2504
|
+
}),
|
|
2505
|
+
prisma2.toolExecutionLog.count({ where })
|
|
2506
|
+
]);
|
|
2507
|
+
const clientIds = /* @__PURE__ */ new Set();
|
|
2508
|
+
logs.forEach((log3) => {
|
|
2509
|
+
if (log3.oauthClientId) clientIds.add(log3.oauthClientId);
|
|
2510
|
+
if (log3.apiKeyId) clientIds.add(log3.apiKeyId);
|
|
2511
|
+
});
|
|
2512
|
+
const [oauthClients, apiKeys] = await Promise.all([
|
|
2513
|
+
clientIds.size > 0 && logs.some((log3) => log3.oauthClientId) ? prisma2.oAuthClient.findMany({
|
|
2514
|
+
where: {
|
|
2515
|
+
id: { in: Array.from(clientIds) }
|
|
2516
|
+
},
|
|
2517
|
+
select: { id: true, clientName: true }
|
|
2518
|
+
}) : Promise.resolve([]),
|
|
2519
|
+
clientIds.size > 0 && logs.some((log3) => log3.apiKeyId) ? prisma2.apiKey.findMany({
|
|
2520
|
+
where: {
|
|
2521
|
+
id: { in: Array.from(clientIds) },
|
|
2522
|
+
tenantId
|
|
2523
|
+
},
|
|
2524
|
+
select: { id: true, name: true }
|
|
2525
|
+
}) : Promise.resolve([])
|
|
2526
|
+
]);
|
|
2527
|
+
const clientNameMap = /* @__PURE__ */ new Map();
|
|
2528
|
+
oauthClients.forEach((client) => {
|
|
2529
|
+
clientNameMap.set(client.id, client.clientName);
|
|
2530
|
+
});
|
|
2531
|
+
apiKeys.forEach((key) => {
|
|
2532
|
+
clientNameMap.set(key.id, key.name);
|
|
2533
|
+
});
|
|
2534
|
+
const responseLogs = logs.filter((log3) => log3.createdAt && !isNaN(new Date(log3.createdAt).getTime())).map((log3) => ({
|
|
2535
|
+
id: log3.id,
|
|
2536
|
+
toolName: log3.toolName,
|
|
2537
|
+
toolModule: log3.toolModule ?? void 0,
|
|
2538
|
+
status: log3.status,
|
|
2539
|
+
clientType: log3.clientType,
|
|
2540
|
+
clientName: log3.oauthClientId ? clientNameMap.get(log3.oauthClientId) : log3.apiKeyId ? clientNameMap.get(log3.apiKeyId) : void 0,
|
|
2541
|
+
shopDomain: log3.shopDomain ?? void 0,
|
|
2542
|
+
durationMs: log3.durationMs ?? void 0,
|
|
2543
|
+
createdAt: new Date(log3.createdAt).toISOString()
|
|
2544
|
+
}));
|
|
2545
|
+
const response = {
|
|
2546
|
+
logs: responseLogs,
|
|
2547
|
+
pagination: {
|
|
2548
|
+
page,
|
|
2549
|
+
limit,
|
|
2550
|
+
total,
|
|
2551
|
+
hasMore: skip + take < total
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
return res.json(response);
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
logger3.error("logs endpoint error", error, {
|
|
2557
|
+
correlationId: req.headers["x-correlation-id"]
|
|
2558
|
+
});
|
|
2559
|
+
return res.status(500).json({
|
|
2560
|
+
error: "Internal Server Error",
|
|
2561
|
+
message: "Failed to fetch activity logs"
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
router.get("/stats", async (req, res) => {
|
|
2566
|
+
try {
|
|
2567
|
+
const tenantId = req.tenantContext?.tenantId;
|
|
2568
|
+
if (!tenantId) {
|
|
2569
|
+
return res.status(401).json({
|
|
2570
|
+
error: "Unauthorized",
|
|
2571
|
+
message: "Tenant context not found"
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
const parsed = activityStatsQuerySchema.safeParse(req.query);
|
|
2575
|
+
if (!parsed.success) {
|
|
2576
|
+
return res.status(400).json({
|
|
2577
|
+
error: "Validation Error",
|
|
2578
|
+
message: "Invalid query parameters",
|
|
2579
|
+
details: parsed.error.errors
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
const { period } = parsed.data;
|
|
2583
|
+
const now = /* @__PURE__ */ new Date();
|
|
2584
|
+
const days = period === "7d" ? 7 : period === "30d" ? 30 : 90;
|
|
2585
|
+
const startDate = new Date(now);
|
|
2586
|
+
startDate.setDate(startDate.getDate() - days);
|
|
2587
|
+
const baseWhere = {
|
|
2588
|
+
tenantId,
|
|
2589
|
+
createdAt: { gte: startDate }
|
|
2590
|
+
};
|
|
2591
|
+
const [totalCalls, successCount, avgDuration, topTools, byClientGroup, byShopGroup] = await Promise.all([
|
|
2592
|
+
// Total calls
|
|
2593
|
+
prisma2.toolExecutionLog.count({ where: baseWhere }),
|
|
2594
|
+
// Success count
|
|
2595
|
+
prisma2.toolExecutionLog.count({
|
|
2596
|
+
where: {
|
|
2597
|
+
...baseWhere,
|
|
2598
|
+
status: "SUCCESS"
|
|
2599
|
+
}
|
|
2600
|
+
}),
|
|
2601
|
+
// Average duration
|
|
2602
|
+
prisma2.toolExecutionLog.aggregate({
|
|
2603
|
+
where: baseWhere,
|
|
2604
|
+
_avg: { durationMs: true }
|
|
2605
|
+
}),
|
|
2606
|
+
// Top 10 tools
|
|
2607
|
+
prisma2.toolExecutionLog.groupBy({
|
|
2608
|
+
by: ["toolName"],
|
|
2609
|
+
where: baseWhere,
|
|
2610
|
+
_count: { id: true },
|
|
2611
|
+
orderBy: { _count: { id: "desc" } },
|
|
2612
|
+
take: 10
|
|
2613
|
+
}),
|
|
2614
|
+
// By client
|
|
2615
|
+
prisma2.toolExecutionLog.groupBy({
|
|
2616
|
+
by: ["clientType", "oauthClientId", "apiKeyId"],
|
|
2617
|
+
where: baseWhere,
|
|
2618
|
+
_count: { id: true }
|
|
2619
|
+
}),
|
|
2620
|
+
// By shop
|
|
2621
|
+
prisma2.toolExecutionLog.groupBy({
|
|
2622
|
+
by: ["shopDomain"],
|
|
2623
|
+
where: {
|
|
2624
|
+
...baseWhere,
|
|
2625
|
+
shopDomain: { not: null }
|
|
2626
|
+
},
|
|
2627
|
+
_count: { id: true }
|
|
2628
|
+
})
|
|
2629
|
+
]);
|
|
2630
|
+
const byDayRaw = await prisma2.$queryRaw`
|
|
2631
|
+
SELECT
|
|
2632
|
+
date_trunc('day', created_at)::date AS date,
|
|
2633
|
+
COUNT(*)::bigint AS calls,
|
|
2634
|
+
COUNT(*) FILTER (WHERE status != 'SUCCESS')::bigint AS errors
|
|
2635
|
+
FROM tool_execution_logs
|
|
2636
|
+
WHERE tenant_id = ${tenantId}
|
|
2637
|
+
AND created_at >= ${startDate}
|
|
2638
|
+
GROUP BY 1
|
|
2639
|
+
ORDER BY 1 ASC
|
|
2640
|
+
`;
|
|
2641
|
+
const clientIds = /* @__PURE__ */ new Set();
|
|
2642
|
+
byClientGroup.forEach((group) => {
|
|
2643
|
+
if (group.oauthClientId) clientIds.add(group.oauthClientId);
|
|
2644
|
+
if (group.apiKeyId) clientIds.add(group.apiKeyId);
|
|
2645
|
+
});
|
|
2646
|
+
const [oauthClients, apiKeys] = await Promise.all([
|
|
2647
|
+
clientIds.size > 0 && byClientGroup.some((g) => g.oauthClientId) ? prisma2.oAuthClient.findMany({
|
|
2648
|
+
where: {
|
|
2649
|
+
id: { in: Array.from(clientIds) }
|
|
2650
|
+
},
|
|
2651
|
+
select: { id: true, clientName: true }
|
|
2652
|
+
}) : Promise.resolve([]),
|
|
2653
|
+
clientIds.size > 0 && byClientGroup.some((g) => g.apiKeyId) ? prisma2.apiKey.findMany({
|
|
2654
|
+
where: {
|
|
2655
|
+
id: { in: Array.from(clientIds) },
|
|
2656
|
+
tenantId
|
|
2657
|
+
},
|
|
2658
|
+
select: { id: true, name: true }
|
|
2659
|
+
}) : Promise.resolve([])
|
|
2660
|
+
]);
|
|
2661
|
+
const clientNameMap = /* @__PURE__ */ new Map();
|
|
2662
|
+
oauthClients.forEach((client) => {
|
|
2663
|
+
clientNameMap.set(client.id, client.clientName);
|
|
2664
|
+
});
|
|
2665
|
+
apiKeys.forEach((key) => {
|
|
2666
|
+
clientNameMap.set(key.id, key.name);
|
|
2667
|
+
});
|
|
2668
|
+
const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
|
|
2669
|
+
const avgDurationMs = avgDuration._avg.durationMs ?? 0;
|
|
2670
|
+
const topToolsFormatted = topTools.map((tool) => ({
|
|
2671
|
+
name: tool.toolName,
|
|
2672
|
+
calls: tool._count.id
|
|
2673
|
+
}));
|
|
2674
|
+
const byClientFormatted = byClientGroup.map((group) => ({
|
|
2675
|
+
clientId: group.oauthClientId ?? void 0,
|
|
2676
|
+
clientName: group.oauthClientId ? clientNameMap.get(group.oauthClientId) : void 0,
|
|
2677
|
+
apiKeyId: group.apiKeyId ?? void 0,
|
|
2678
|
+
keyName: group.apiKeyId ? clientNameMap.get(group.apiKeyId) : void 0,
|
|
2679
|
+
type: group.clientType === "oauth_client" ? "oauth" : "api_key",
|
|
2680
|
+
calls: group._count.id
|
|
2681
|
+
}));
|
|
2682
|
+
const byShopFormatted = byShopGroup.filter((group) => group.shopDomain).map((group) => ({
|
|
2683
|
+
shopDomain: group.shopDomain,
|
|
2684
|
+
calls: group._count.id
|
|
2685
|
+
}));
|
|
2686
|
+
const byDayFormatted = byDayRaw.filter((row) => row.date && !isNaN(new Date(row.date).getTime())).map((row) => ({
|
|
2687
|
+
date: new Date(row.date).toISOString().split("T")[0],
|
|
2688
|
+
// YYYY-MM-DD
|
|
2689
|
+
calls: Number(row.calls),
|
|
2690
|
+
errors: Number(row.errors)
|
|
2691
|
+
}));
|
|
2692
|
+
const response = {
|
|
2693
|
+
period,
|
|
2694
|
+
totalCalls,
|
|
2695
|
+
successRate: Math.round(successRate * 100) / 100,
|
|
2696
|
+
// Round to 2 decimal places
|
|
2697
|
+
avgDurationMs: Math.round(avgDurationMs),
|
|
2698
|
+
topTools: topToolsFormatted,
|
|
2699
|
+
byClient: byClientFormatted,
|
|
2700
|
+
byShop: byShopFormatted,
|
|
2701
|
+
byDay: byDayFormatted
|
|
2702
|
+
};
|
|
2703
|
+
return res.json(response);
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
logger3.error("stats endpoint error", error, {
|
|
2706
|
+
correlationId: req.headers["x-correlation-id"]
|
|
2707
|
+
});
|
|
2708
|
+
return res.status(500).json({
|
|
2709
|
+
error: "Internal Server Error",
|
|
2710
|
+
message: "Failed to compute activity statistics"
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2650
2714
|
return router;
|
|
2651
2715
|
}
|
|
2652
2716
|
|
|
2653
2717
|
// src/api/oauth-clients.ts
|
|
2654
2718
|
import { Router as Router6 } from "express";
|
|
2719
|
+
var logger4 = createLogger("api/oauth-clients");
|
|
2655
2720
|
var OAUTH_CLIENT_REVOKE_ACTION = "oauth_client:revoke";
|
|
2656
2721
|
function createOAuthClientsRouter(options) {
|
|
2657
2722
|
const { auditLogger: auditLogger2 } = options;
|
|
@@ -2676,6 +2741,129 @@ function createOAuthClientsRouter(options) {
|
|
|
2676
2741
|
});
|
|
2677
2742
|
}
|
|
2678
2743
|
});
|
|
2744
|
+
router.get("/:clientId/metrics", async (req, res) => {
|
|
2745
|
+
try {
|
|
2746
|
+
const tenantId = req.tenantContext?.tenantId;
|
|
2747
|
+
if (!tenantId) {
|
|
2748
|
+
return res.status(401).json({
|
|
2749
|
+
error: "Unauthorized",
|
|
2750
|
+
message: "Tenant context not found"
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
const { clientId } = req.params;
|
|
2754
|
+
const clientExists = await prisma2.oAuthRefreshToken.findFirst({
|
|
2755
|
+
where: {
|
|
2756
|
+
clientId,
|
|
2757
|
+
tenantId,
|
|
2758
|
+
revokedAt: null
|
|
2759
|
+
}
|
|
2760
|
+
});
|
|
2761
|
+
if (!clientExists) {
|
|
2762
|
+
return res.status(404).json({
|
|
2763
|
+
error: "Not Found",
|
|
2764
|
+
message: "OAuth client not found or no active tokens"
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
const now = /* @__PURE__ */ new Date();
|
|
2768
|
+
const sevenDaysAgo = new Date(now);
|
|
2769
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
2770
|
+
const thirtyDaysAgo = new Date(now);
|
|
2771
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
2772
|
+
const baseWhere = {
|
|
2773
|
+
tenantId,
|
|
2774
|
+
oauthClientId: clientId
|
|
2775
|
+
};
|
|
2776
|
+
const queryStartTime = Date.now();
|
|
2777
|
+
const [calls7d, calls30d, totalCalls, successCount, avgDuration, topTools, lastUsed] = await Promise.all([
|
|
2778
|
+
// Calls in last 7 days
|
|
2779
|
+
prisma2.toolExecutionLog.count({
|
|
2780
|
+
where: {
|
|
2781
|
+
...baseWhere,
|
|
2782
|
+
createdAt: { gte: sevenDaysAgo }
|
|
2783
|
+
}
|
|
2784
|
+
}),
|
|
2785
|
+
// Calls in last 30 days
|
|
2786
|
+
prisma2.toolExecutionLog.count({
|
|
2787
|
+
where: {
|
|
2788
|
+
...baseWhere,
|
|
2789
|
+
createdAt: { gte: thirtyDaysAgo }
|
|
2790
|
+
}
|
|
2791
|
+
}),
|
|
2792
|
+
// Total calls (for success rate calculation)
|
|
2793
|
+
prisma2.toolExecutionLog.count({
|
|
2794
|
+
where: {
|
|
2795
|
+
...baseWhere,
|
|
2796
|
+
createdAt: { gte: sevenDaysAgo }
|
|
2797
|
+
}
|
|
2798
|
+
}),
|
|
2799
|
+
// Success count (for success rate calculation)
|
|
2800
|
+
prisma2.toolExecutionLog.count({
|
|
2801
|
+
where: {
|
|
2802
|
+
...baseWhere,
|
|
2803
|
+
createdAt: { gte: sevenDaysAgo },
|
|
2804
|
+
status: "SUCCESS"
|
|
2805
|
+
}
|
|
2806
|
+
}),
|
|
2807
|
+
// Average duration
|
|
2808
|
+
prisma2.toolExecutionLog.aggregate({
|
|
2809
|
+
where: {
|
|
2810
|
+
...baseWhere,
|
|
2811
|
+
createdAt: { gte: sevenDaysAgo },
|
|
2812
|
+
durationMs: { not: null }
|
|
2813
|
+
},
|
|
2814
|
+
_avg: { durationMs: true }
|
|
2815
|
+
}),
|
|
2816
|
+
// Top 3 tools by call count in last 7 days
|
|
2817
|
+
prisma2.toolExecutionLog.groupBy({
|
|
2818
|
+
by: ["toolName"],
|
|
2819
|
+
where: {
|
|
2820
|
+
...baseWhere,
|
|
2821
|
+
createdAt: { gte: sevenDaysAgo }
|
|
2822
|
+
},
|
|
2823
|
+
_count: { id: true },
|
|
2824
|
+
orderBy: { _count: { id: "desc" } },
|
|
2825
|
+
take: 3
|
|
2826
|
+
}),
|
|
2827
|
+
// Most recent execution
|
|
2828
|
+
prisma2.toolExecutionLog.findFirst({
|
|
2829
|
+
where: baseWhere,
|
|
2830
|
+
orderBy: { createdAt: "desc" },
|
|
2831
|
+
select: { createdAt: true }
|
|
2832
|
+
})
|
|
2833
|
+
]);
|
|
2834
|
+
const successRate = totalCalls > 0 ? successCount / totalCalls * 100 : 0;
|
|
2835
|
+
const topToolsFormatted = topTools.map((tool) => ({
|
|
2836
|
+
name: tool.toolName,
|
|
2837
|
+
calls: tool._count.id
|
|
2838
|
+
}));
|
|
2839
|
+
const queryDuration = Date.now() - queryStartTime;
|
|
2840
|
+
if (queryDuration > 200) {
|
|
2841
|
+
logger4.warn("OAuth client metrics query exceeded 200ms", {
|
|
2842
|
+
clientId,
|
|
2843
|
+
tenantId,
|
|
2844
|
+
durationMs: queryDuration
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
const response = {
|
|
2848
|
+
calls7d,
|
|
2849
|
+
calls30d,
|
|
2850
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
2851
|
+
// Round to 1 decimal place
|
|
2852
|
+
avgDurationMs: Math.round(avgDuration._avg.durationMs ?? 0),
|
|
2853
|
+
topTools: topToolsFormatted,
|
|
2854
|
+
lastUsedAt: lastUsed?.createdAt.toISOString() ?? null
|
|
2855
|
+
};
|
|
2856
|
+
return res.json(response);
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
logger4.error("metrics endpoint error", error, {
|
|
2859
|
+
correlationId: req.headers["x-correlation-id"]
|
|
2860
|
+
});
|
|
2861
|
+
return res.status(500).json({
|
|
2862
|
+
error: "Internal Server Error",
|
|
2863
|
+
message: "Failed to compute OAuth client metrics"
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
});
|
|
2679
2867
|
router.delete("/:clientId", async (req, res) => {
|
|
2680
2868
|
try {
|
|
2681
2869
|
const tenantId = req.tenantContext?.tenantId;
|
|
@@ -2826,7 +3014,7 @@ var oauthClientsRouter = createOAuthClientsRouter({
|
|
|
2826
3014
|
});
|
|
2827
3015
|
|
|
2828
3016
|
// src/lifecycle/shutdown.ts
|
|
2829
|
-
var
|
|
3017
|
+
var logger5 = createLogger("lifecycle/shutdown");
|
|
2830
3018
|
var ShutdownManager = class {
|
|
2831
3019
|
/** Current shutdown state */
|
|
2832
3020
|
_state = "running";
|
|
@@ -2956,20 +3144,20 @@ var ShutdownManager = class {
|
|
|
2956
3144
|
*/
|
|
2957
3145
|
registerSignalHandlers() {
|
|
2958
3146
|
process.on("SIGTERM", () => {
|
|
2959
|
-
|
|
3147
|
+
logger5.info("Received SIGTERM signal");
|
|
2960
3148
|
this.initiateShutdown().catch((error) => {
|
|
2961
|
-
|
|
3149
|
+
logger5.error("Shutdown failed", error instanceof Error ? error : void 0);
|
|
2962
3150
|
process.exit(1);
|
|
2963
3151
|
});
|
|
2964
3152
|
});
|
|
2965
3153
|
process.on("SIGINT", () => {
|
|
2966
|
-
|
|
3154
|
+
logger5.info("Received SIGINT signal");
|
|
2967
3155
|
this.initiateShutdown().catch((error) => {
|
|
2968
|
-
|
|
3156
|
+
logger5.error("Shutdown failed", error instanceof Error ? error : void 0);
|
|
2969
3157
|
process.exit(1);
|
|
2970
3158
|
});
|
|
2971
3159
|
});
|
|
2972
|
-
|
|
3160
|
+
logger5.debug("Signal handlers registered (SIGTERM, SIGINT)");
|
|
2973
3161
|
}
|
|
2974
3162
|
/**
|
|
2975
3163
|
* Initiate graceful shutdown
|
|
@@ -2987,13 +3175,13 @@ var ShutdownManager = class {
|
|
|
2987
3175
|
*/
|
|
2988
3176
|
async initiateShutdown() {
|
|
2989
3177
|
if (this.shutdownInProgress) {
|
|
2990
|
-
|
|
3178
|
+
logger5.debug("Shutdown already in progress, ignoring duplicate call");
|
|
2991
3179
|
return;
|
|
2992
3180
|
}
|
|
2993
3181
|
this.shutdownInProgress = true;
|
|
2994
3182
|
this._state = "draining";
|
|
2995
3183
|
this.drainingStartedAt = /* @__PURE__ */ new Date();
|
|
2996
|
-
|
|
3184
|
+
logger5.info("Shutdown initiated", {
|
|
2997
3185
|
drainTimeoutMs: this.drainTimeoutMs,
|
|
2998
3186
|
activeConnections: this.getRemainingConnections()
|
|
2999
3187
|
});
|
|
@@ -3009,11 +3197,11 @@ var ShutdownManager = class {
|
|
|
3009
3197
|
}
|
|
3010
3198
|
if (this.mcpSessionsCleanup) {
|
|
3011
3199
|
try {
|
|
3012
|
-
|
|
3200
|
+
logger5.debug("Closing MCP sessions");
|
|
3013
3201
|
await this.withTimeout(this.mcpSessionsCleanup(), 5e3, "MCP sessions cleanup");
|
|
3014
|
-
|
|
3202
|
+
logger5.debug("MCP sessions closed");
|
|
3015
3203
|
} catch (error) {
|
|
3016
|
-
|
|
3204
|
+
logger5.error("Failed to close MCP sessions", error instanceof Error ? error : void 0);
|
|
3017
3205
|
}
|
|
3018
3206
|
}
|
|
3019
3207
|
const summary = await this.runCleanupCallbacks();
|
|
@@ -3031,14 +3219,14 @@ var ShutdownManager = class {
|
|
|
3031
3219
|
resolve();
|
|
3032
3220
|
return;
|
|
3033
3221
|
}
|
|
3034
|
-
|
|
3222
|
+
logger5.debug("Stopping HTTP server from accepting new connections");
|
|
3035
3223
|
this.httpServer.close((error) => {
|
|
3036
3224
|
if (error) {
|
|
3037
3225
|
if (error.code !== "ERR_SERVER_NOT_RUNNING") {
|
|
3038
|
-
|
|
3226
|
+
logger5.warn("HTTP server close error", { error: error.message });
|
|
3039
3227
|
}
|
|
3040
3228
|
}
|
|
3041
|
-
|
|
3229
|
+
logger5.debug("HTTP server stopped accepting connections");
|
|
3042
3230
|
resolve();
|
|
3043
3231
|
});
|
|
3044
3232
|
});
|
|
@@ -3051,7 +3239,7 @@ var ShutdownManager = class {
|
|
|
3051
3239
|
while (this.getRemainingConnections() > 0) {
|
|
3052
3240
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
3053
3241
|
}
|
|
3054
|
-
|
|
3242
|
+
logger5.debug("All connections drained");
|
|
3055
3243
|
}
|
|
3056
3244
|
/**
|
|
3057
3245
|
* Create drain timeout promise
|
|
@@ -3059,7 +3247,7 @@ var ShutdownManager = class {
|
|
|
3059
3247
|
createDrainTimeout() {
|
|
3060
3248
|
return new Promise((resolve) => {
|
|
3061
3249
|
this.drainTimeoutTimer = setTimeout(() => {
|
|
3062
|
-
|
|
3250
|
+
logger5.warn("Drain timeout reached, proceeding with shutdown", {
|
|
3063
3251
|
remainingConnections: this.getRemainingConnections(),
|
|
3064
3252
|
drainTimeoutMs: this.drainTimeoutMs
|
|
3065
3253
|
});
|
|
@@ -3074,7 +3262,7 @@ var ShutdownManager = class {
|
|
|
3074
3262
|
async runCleanupCallbacks() {
|
|
3075
3263
|
const drainTimeMs = this.drainingStartedAt ? Date.now() - this.drainingStartedAt.getTime() : 0;
|
|
3076
3264
|
let cleanupErrors = 0;
|
|
3077
|
-
|
|
3265
|
+
logger5.debug("Running cleanup callbacks", {
|
|
3078
3266
|
count: this.cleanupCallbacks.length
|
|
3079
3267
|
});
|
|
3080
3268
|
for (let i = 0; i < this.cleanupCallbacks.length; i++) {
|
|
@@ -3087,7 +3275,7 @@ var ShutdownManager = class {
|
|
|
3087
3275
|
);
|
|
3088
3276
|
} catch (error) {
|
|
3089
3277
|
cleanupErrors++;
|
|
3090
|
-
|
|
3278
|
+
logger5.error(
|
|
3091
3279
|
`Cleanup callback ${i + 1} failed`,
|
|
3092
3280
|
error instanceof Error ? error : void 0
|
|
3093
3281
|
);
|
|
@@ -3121,7 +3309,7 @@ var ShutdownManager = class {
|
|
|
3121
3309
|
* Log final shutdown summary
|
|
3122
3310
|
*/
|
|
3123
3311
|
logShutdownSummary(summary) {
|
|
3124
|
-
|
|
3312
|
+
logger5.info("Shutdown complete", {
|
|
3125
3313
|
drainTimeMs: summary.drainTimeMs,
|
|
3126
3314
|
cleanupCallbacksRun: summary.cleanupCallbacksRun,
|
|
3127
3315
|
cleanupErrors: summary.cleanupErrors,
|
|
@@ -3208,36 +3396,6 @@ var ShutdownManager = class {
|
|
|
3208
3396
|
};
|
|
3209
3397
|
var shutdownManager = new ShutdownManager();
|
|
3210
3398
|
|
|
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
3399
|
// src/metrics/registry.ts
|
|
3242
3400
|
import { Counter, Gauge, Histogram, Registry, collectDefaultMetrics } from "prom-client";
|
|
3243
3401
|
var METRICS_PREFIX = "shopify_mcp_";
|
|
@@ -3920,10 +4078,10 @@ var OAuthDiscoveryService = class {
|
|
|
3920
4078
|
// Used by ChatGPT for automatic client registration
|
|
3921
4079
|
registration_endpoint: `${this.baseUrl}/oauth/register`,
|
|
3922
4080
|
// Supported authentication methods at token endpoint
|
|
3923
|
-
// Client authentication is required for token exchange
|
|
3924
4081
|
// "client_secret_post" - credentials in request body (client_id, client_secret)
|
|
3925
4082
|
// "client_secret_basic" - credentials in Authorization header (Basic auth)
|
|
3926
|
-
|
|
4083
|
+
// "none" - public client with PKCE only (required for Claude.ai)
|
|
4084
|
+
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic", "none"],
|
|
3927
4085
|
// Supported grant types
|
|
3928
4086
|
// authorization_code: Standard OAuth 2.0 flow with PKCE
|
|
3929
4087
|
// refresh_token: For obtaining new access tokens
|
|
@@ -3931,6 +4089,9 @@ var OAuthDiscoveryService = class {
|
|
|
3931
4089
|
// Response types for authorization endpoint
|
|
3932
4090
|
// "code" for authorization code flow
|
|
3933
4091
|
response_types_supported: ["code"],
|
|
4092
|
+
// Response modes for authorization endpoint
|
|
4093
|
+
// "query" returns code in URL query string (standard for auth code flow)
|
|
4094
|
+
response_modes_supported: ["query"],
|
|
3934
4095
|
// PKCE code challenge methods supported (when PKCE is used)
|
|
3935
4096
|
// S256 is the only method supported - plain is NOT allowed
|
|
3936
4097
|
// Note: PKCE is optional for confidential clients (with client_secret)
|
|
@@ -4014,6 +4175,7 @@ var CLIENT_SECRET_BYTES = 16;
|
|
|
4014
4175
|
var BCRYPT_COST_FACTOR2 = 12;
|
|
4015
4176
|
var DEFAULT_GRANT_TYPES = ["authorization_code", "refresh_token"];
|
|
4016
4177
|
var DEFAULT_SCOPES3 = ["mcp:full"];
|
|
4178
|
+
var PUBLIC_CLIENT_MARKER = "PUBLIC_CLIENT:none";
|
|
4017
4179
|
var OAuthRegistrationService = class {
|
|
4018
4180
|
prisma;
|
|
4019
4181
|
/**
|
|
@@ -4084,22 +4246,40 @@ var OAuthRegistrationService = class {
|
|
|
4084
4246
|
* hashed secret in the database, and returns RFC 7591 compliant
|
|
4085
4247
|
* response with the plaintext secret (one-time reveal).
|
|
4086
4248
|
*
|
|
4249
|
+
* Supports both confidential clients (with client_secret) and
|
|
4250
|
+
* public clients (token_endpoint_auth_method: "none", uses PKCE).
|
|
4251
|
+
*
|
|
4087
4252
|
* @param request - RFC 7591 client registration request
|
|
4088
4253
|
* @returns RFC 7591 compliant registration response
|
|
4089
4254
|
*
|
|
4090
4255
|
* @throws Error if database operation fails
|
|
4091
4256
|
*
|
|
4092
4257
|
* @example
|
|
4258
|
+
* // Confidential client (default)
|
|
4093
4259
|
* const response = await service.registerClient({
|
|
4094
4260
|
* client_name: "ChatGPT MCP Client",
|
|
4095
4261
|
* redirect_uris: ["https://chatgpt.com/aip/g/callback"]
|
|
4096
4262
|
* });
|
|
4097
|
-
*
|
|
4263
|
+
*
|
|
4264
|
+
* @example
|
|
4265
|
+
* // Public client (no secret, uses PKCE) - required for Claude.ai
|
|
4266
|
+
* const response = await service.registerClient({
|
|
4267
|
+
* client_name: "Claude AI",
|
|
4268
|
+
* redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
|
|
4269
|
+
* token_endpoint_auth_method: "none"
|
|
4270
|
+
* });
|
|
4098
4271
|
*/
|
|
4099
4272
|
async registerClient(request) {
|
|
4100
4273
|
const clientId = this.generateClientId();
|
|
4101
|
-
const
|
|
4102
|
-
|
|
4274
|
+
const isPublicClient = request.token_endpoint_auth_method === "none";
|
|
4275
|
+
let clientSecret;
|
|
4276
|
+
let clientSecretHash;
|
|
4277
|
+
if (isPublicClient) {
|
|
4278
|
+
clientSecretHash = PUBLIC_CLIENT_MARKER;
|
|
4279
|
+
} else {
|
|
4280
|
+
clientSecret = this.generateClientSecret();
|
|
4281
|
+
clientSecretHash = await this.hashClientSecret(clientSecret);
|
|
4282
|
+
}
|
|
4103
4283
|
const grantTypes = request.grant_types?.length ? request.grant_types : DEFAULT_GRANT_TYPES;
|
|
4104
4284
|
const client = await this.prisma.oAuthClient.create({
|
|
4105
4285
|
data: {
|
|
@@ -4111,10 +4291,8 @@ var OAuthRegistrationService = class {
|
|
|
4111
4291
|
scopes: DEFAULT_SCOPES3
|
|
4112
4292
|
}
|
|
4113
4293
|
});
|
|
4114
|
-
|
|
4294
|
+
const response = {
|
|
4115
4295
|
client_id: clientId,
|
|
4116
|
-
client_secret: clientSecret,
|
|
4117
|
-
// One-time reveal!
|
|
4118
4296
|
client_name: request.client_name,
|
|
4119
4297
|
redirect_uris: request.redirect_uris,
|
|
4120
4298
|
grant_types: grantTypes,
|
|
@@ -4122,6 +4300,10 @@ var OAuthRegistrationService = class {
|
|
|
4122
4300
|
client_secret_expires_at: 0
|
|
4123
4301
|
// Per RFC 7591 Section 3.2.1: 0 means no expiry
|
|
4124
4302
|
};
|
|
4303
|
+
if (clientSecret) {
|
|
4304
|
+
response.client_secret = clientSecret;
|
|
4305
|
+
}
|
|
4306
|
+
return response;
|
|
4125
4307
|
}
|
|
4126
4308
|
/**
|
|
4127
4309
|
* Find a client by client_id
|
|
@@ -4632,15 +4814,16 @@ var OAuthTokenService = class {
|
|
|
4632
4814
|
/**
|
|
4633
4815
|
* Authenticate a client from request
|
|
4634
4816
|
*
|
|
4635
|
-
* Supports
|
|
4817
|
+
* Supports three authentication methods:
|
|
4636
4818
|
* - client_secret_post: client_id and client_secret in request body
|
|
4637
4819
|
* - client_secret_basic: HTTP Basic auth header
|
|
4820
|
+
* - none: public client (no secret), requires PKCE - used by Claude.ai
|
|
4638
4821
|
*
|
|
4639
4822
|
* Per RFC 6749 Section 2.3.1
|
|
4640
4823
|
*
|
|
4641
4824
|
* @param params - Token request parameters (may contain client_id/secret)
|
|
4642
4825
|
* @param authorizationHeader - Authorization header value (if present)
|
|
4643
|
-
* @returns Authenticated client entity
|
|
4826
|
+
* @returns Authenticated client entity with isPublicClient flag
|
|
4644
4827
|
* @throws OAuthTokenError if authentication fails
|
|
4645
4828
|
*/
|
|
4646
4829
|
async authenticateClient(params, authorizationHeader) {
|
|
@@ -4675,15 +4858,18 @@ var OAuthTokenService = class {
|
|
|
4675
4858
|
has_client_secret: !!clientSecret,
|
|
4676
4859
|
secret_length: clientSecret?.length || 0
|
|
4677
4860
|
});
|
|
4678
|
-
if (!clientId
|
|
4679
|
-
|
|
4680
|
-
|
|
4861
|
+
if (!clientId) {
|
|
4862
|
+
clientId = params.client_id;
|
|
4863
|
+
}
|
|
4864
|
+
if (!clientId) {
|
|
4865
|
+
log2.warn("Client authentication failed: missing client_id", {
|
|
4866
|
+
has_client_id: false,
|
|
4681
4867
|
has_client_secret: !!clientSecret,
|
|
4682
4868
|
auth_method: authMethod
|
|
4683
4869
|
});
|
|
4684
4870
|
throw new OAuthTokenError(
|
|
4685
4871
|
"invalid_client",
|
|
4686
|
-
"Client authentication required. Provide client_id
|
|
4872
|
+
"Client authentication required. Provide client_id.",
|
|
4687
4873
|
401
|
|
4688
4874
|
);
|
|
4689
4875
|
}
|
|
@@ -4696,6 +4882,31 @@ var OAuthTokenService = class {
|
|
|
4696
4882
|
});
|
|
4697
4883
|
throw new OAuthTokenError("invalid_client", "Unknown client_id", 401);
|
|
4698
4884
|
}
|
|
4885
|
+
const isPublicClient = client.clientSecretHash === PUBLIC_CLIENT_MARKER;
|
|
4886
|
+
if (isPublicClient) {
|
|
4887
|
+
log2.info("Public client authenticated (no secret required, PKCE enforced)", {
|
|
4888
|
+
client_id: clientId,
|
|
4889
|
+
client_name: client.clientName,
|
|
4890
|
+
auth_method: "none"
|
|
4891
|
+
});
|
|
4892
|
+
return {
|
|
4893
|
+
clientId: client.clientId,
|
|
4894
|
+
clientSecretHash: client.clientSecretHash,
|
|
4895
|
+
clientName: client.clientName,
|
|
4896
|
+
isPublicClient: true
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4899
|
+
if (!clientSecret) {
|
|
4900
|
+
log2.warn("Client authentication failed: missing client_secret for confidential client", {
|
|
4901
|
+
client_id: clientId,
|
|
4902
|
+
client_name: client.clientName
|
|
4903
|
+
});
|
|
4904
|
+
throw new OAuthTokenError(
|
|
4905
|
+
"invalid_client",
|
|
4906
|
+
"Client authentication required. Provide client_id and client_secret.",
|
|
4907
|
+
401
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
4699
4910
|
const isValidSecret = await bcrypt5.compare(clientSecret, client.clientSecretHash);
|
|
4700
4911
|
if (!isValidSecret) {
|
|
4701
4912
|
log2.warn("Client authentication failed: invalid client_secret", {
|
|
@@ -4712,7 +4923,8 @@ var OAuthTokenService = class {
|
|
|
4712
4923
|
return {
|
|
4713
4924
|
clientId: client.clientId,
|
|
4714
4925
|
clientSecretHash: client.clientSecretHash,
|
|
4715
|
-
clientName: client.clientName
|
|
4926
|
+
clientName: client.clientName,
|
|
4927
|
+
isPublicClient: false
|
|
4716
4928
|
};
|
|
4717
4929
|
}
|
|
4718
4930
|
// ===========================================================================
|
|
@@ -4850,6 +5062,19 @@ var OAuthTokenService = class {
|
|
|
4850
5062
|
});
|
|
4851
5063
|
throw new OAuthTokenError("invalid_grant", "Authorization code has already been used");
|
|
4852
5064
|
}
|
|
5065
|
+
const client = await this.prisma.oAuthClient.findUnique({
|
|
5066
|
+
where: { clientId }
|
|
5067
|
+
});
|
|
5068
|
+
const isPublicClient = client?.clientSecretHash === PUBLIC_CLIENT_MARKER;
|
|
5069
|
+
if (isPublicClient && !authCode.codeChallenge) {
|
|
5070
|
+
log2.warn("Token exchange failed: public client did not use PKCE", {
|
|
5071
|
+
client_id: clientId
|
|
5072
|
+
});
|
|
5073
|
+
throw new OAuthTokenError(
|
|
5074
|
+
"invalid_grant",
|
|
5075
|
+
"Public clients must use PKCE. No code_challenge was provided during authorization."
|
|
5076
|
+
);
|
|
5077
|
+
}
|
|
4853
5078
|
if (authCode.codeChallenge) {
|
|
4854
5079
|
if (!params.code_verifier) {
|
|
4855
5080
|
throw new OAuthTokenError(
|
|
@@ -5072,7 +5297,10 @@ var clientRegistrationRequestSchema = z6.object({
|
|
|
5072
5297
|
invalid_type_error: "redirect_uris must be an array"
|
|
5073
5298
|
}).min(1, "redirect_uris cannot be empty"),
|
|
5074
5299
|
grant_types: z6.array(z6.string()).optional(),
|
|
5075
|
-
response_types: z6.array(z6.string()).optional()
|
|
5300
|
+
response_types: z6.array(z6.string()).optional(),
|
|
5301
|
+
// Token endpoint authentication method (RFC 7591)
|
|
5302
|
+
// 'none' = public client (no client_secret, uses PKCE) - required for Claude.ai
|
|
5303
|
+
token_endpoint_auth_method: z6.enum(["client_secret_post", "client_secret_basic", "none"]).optional()
|
|
5076
5304
|
});
|
|
5077
5305
|
function validateClientRegistrationRequest(data) {
|
|
5078
5306
|
const result = clientRegistrationRequestSchema.safeParse(data);
|
|
@@ -5101,6 +5329,7 @@ var TokenRefreshService = class {
|
|
|
5101
5329
|
cryptoService;
|
|
5102
5330
|
config;
|
|
5103
5331
|
refreshInterval = null;
|
|
5332
|
+
clientPool = null;
|
|
5104
5333
|
constructor(prisma2, cryptoService, config) {
|
|
5105
5334
|
this.prisma = prisma2;
|
|
5106
5335
|
this.cryptoService = cryptoService;
|
|
@@ -5112,6 +5341,14 @@ var TokenRefreshService = class {
|
|
|
5112
5341
|
};
|
|
5113
5342
|
log.debug("TokenRefreshService initialized");
|
|
5114
5343
|
}
|
|
5344
|
+
/**
|
|
5345
|
+
* Set the client pool for cache eviction after token refresh
|
|
5346
|
+
* Must be called after both services are initialized to avoid circular dependency
|
|
5347
|
+
*/
|
|
5348
|
+
setClientPool(clientPool) {
|
|
5349
|
+
this.clientPool = clientPool;
|
|
5350
|
+
log.debug("TokenRefreshService: client pool reference set");
|
|
5351
|
+
}
|
|
5115
5352
|
/**
|
|
5116
5353
|
* Refresh an access token using the refresh token
|
|
5117
5354
|
*
|
|
@@ -5200,6 +5437,12 @@ var TokenRefreshService = class {
|
|
|
5200
5437
|
where: { id: shop.id },
|
|
5201
5438
|
data: updateData
|
|
5202
5439
|
});
|
|
5440
|
+
if (this.clientPool) {
|
|
5441
|
+
this.clientPool.evict(shop.tenantId, shop.shopDomain);
|
|
5442
|
+
log.debug(
|
|
5443
|
+
`Evicted cached client for ${shop.shopDomain.substring(0, 20)} after token refresh`
|
|
5444
|
+
);
|
|
5445
|
+
}
|
|
5203
5446
|
log.info(
|
|
5204
5447
|
`Token refreshed successfully for ${shop.shopDomain.substring(0, 20)} (expires in ${tokenData.expires_in}s)`
|
|
5205
5448
|
);
|
|
@@ -6073,8 +6316,8 @@ async function createHttpTransport(server) {
|
|
|
6073
6316
|
);
|
|
6074
6317
|
log.info(`CORS middleware enabled with ${allowedOrigins.length} allowed origin(s)`);
|
|
6075
6318
|
app.use((req, res, next) => {
|
|
6076
|
-
const
|
|
6077
|
-
const frameAncestors =
|
|
6319
|
+
const isOAuthRelatedPage = req.path.startsWith("/oauth/authorize") || req.path.startsWith("/app/oauth/") || req.path.startsWith("/app/login") || req.path === "/app" || req.path === "/app/";
|
|
6320
|
+
const frameAncestors = isOAuthRelatedPage ? "frame-ancestors 'self' https://claude.ai https://*.anthropic.com https://*.claude.ai" : "frame-ancestors 'none'";
|
|
6078
6321
|
res.setHeader(
|
|
6079
6322
|
"Content-Security-Policy",
|
|
6080
6323
|
[
|
|
@@ -6086,7 +6329,8 @@ async function createHttpTransport(server) {
|
|
|
6086
6329
|
"connect-src 'self' https://*.myshopify.com",
|
|
6087
6330
|
frameAncestors,
|
|
6088
6331
|
"base-uri 'self'",
|
|
6089
|
-
|
|
6332
|
+
// Allow form submissions to Claude and Anthropic domains for OAuth redirects
|
|
6333
|
+
"form-action 'self' https://claude.ai https://*.claude.ai https://*.anthropic.com"
|
|
6090
6334
|
].join("; ")
|
|
6091
6335
|
);
|
|
6092
6336
|
next();
|
|
@@ -6111,6 +6355,9 @@ async function createHttpTransport(server) {
|
|
|
6111
6355
|
clientId: config.SHOPIFY_CLIENT_ID,
|
|
6112
6356
|
clientSecret: config.SHOPIFY_CLIENT_SECRET
|
|
6113
6357
|
});
|
|
6358
|
+
if (clientPool) {
|
|
6359
|
+
tokenRefreshService.setClientPool(clientPool);
|
|
6360
|
+
}
|
|
6114
6361
|
tokenRefreshService.startBackgroundRefresh();
|
|
6115
6362
|
log.info("TokenRefreshService initialized (background refresh enabled)");
|
|
6116
6363
|
} else {
|
|
@@ -6205,6 +6452,50 @@ async function createHttpTransport(server) {
|
|
|
6205
6452
|
}
|
|
6206
6453
|
next();
|
|
6207
6454
|
});
|
|
6455
|
+
app.head("/mcp", async (req, res) => {
|
|
6456
|
+
const authHeader = req.headers.authorization;
|
|
6457
|
+
const bearerToken = typeof authHeader === "string" && authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7) : void 0;
|
|
6458
|
+
log.info("[mcp] HEAD /mcp probe request", {
|
|
6459
|
+
hasAuth: !!bearerToken,
|
|
6460
|
+
userAgent: req.headers["user-agent"]
|
|
6461
|
+
});
|
|
6462
|
+
if (!bearerToken) {
|
|
6463
|
+
const wwwAuth2 = oauthDiscoveryService.getWwwAuthenticateHeader(
|
|
6464
|
+
"invalid_token",
|
|
6465
|
+
"Authentication required"
|
|
6466
|
+
);
|
|
6467
|
+
res.setHeader("WWW-Authenticate", wwwAuth2);
|
|
6468
|
+
res.status(401).json({
|
|
6469
|
+
error: "unauthorized",
|
|
6470
|
+
message: "Authentication required. Initiate OAuth flow."
|
|
6471
|
+
});
|
|
6472
|
+
return;
|
|
6473
|
+
}
|
|
6474
|
+
if (apiKeyService) {
|
|
6475
|
+
try {
|
|
6476
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6477
|
+
const authResult = await validateMcpBearerToken(
|
|
6478
|
+
`Bearer ${bearerToken}`,
|
|
6479
|
+
apiKeyService,
|
|
6480
|
+
getPrismaClient()
|
|
6481
|
+
);
|
|
6482
|
+
if (authResult.valid) {
|
|
6483
|
+
res.status(200).end();
|
|
6484
|
+
return;
|
|
6485
|
+
}
|
|
6486
|
+
} catch {
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6489
|
+
const wwwAuth = oauthDiscoveryService.getWwwAuthenticateHeader(
|
|
6490
|
+
"invalid_token",
|
|
6491
|
+
"Invalid or expired token"
|
|
6492
|
+
);
|
|
6493
|
+
res.setHeader("WWW-Authenticate", wwwAuth);
|
|
6494
|
+
res.status(401).json({
|
|
6495
|
+
error: "unauthorized",
|
|
6496
|
+
message: "Invalid or expired token"
|
|
6497
|
+
});
|
|
6498
|
+
});
|
|
6208
6499
|
app.post("/mcp", async (req, res) => {
|
|
6209
6500
|
try {
|
|
6210
6501
|
const authHeader = req.headers.authorization;
|
|
@@ -6252,7 +6543,7 @@ async function createHttpTransport(server) {
|
|
|
6252
6543
|
}
|
|
6253
6544
|
if (sessionContext && methodRequiresShop && isRemote && apiKeyService && clientPool && prisma2 && (!sessionContext.requestContext?.client || !sessionContext.requestContext?.shopDomain)) {
|
|
6254
6545
|
log.debug(`[mcp] Lazy loading context for existing session: ${sessionPrefix}...`);
|
|
6255
|
-
const { validateMcpBearerToken } = await import("./mcp-auth-
|
|
6546
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6256
6547
|
const authResult = await validateMcpBearerToken(
|
|
6257
6548
|
req.headers.authorization,
|
|
6258
6549
|
apiKeyService,
|
|
@@ -6356,7 +6647,7 @@ async function createHttpTransport(server) {
|
|
|
6356
6647
|
let shopifyClient;
|
|
6357
6648
|
let multiTenantContext;
|
|
6358
6649
|
if (isRemote && apiKeyService && clientPool && prisma2) {
|
|
6359
|
-
const { validateMcpBearerToken } = await import("./mcp-auth-
|
|
6650
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6360
6651
|
const authResult = await validateMcpBearerToken(
|
|
6361
6652
|
req.headers.authorization,
|
|
6362
6653
|
apiKeyService,
|
|
@@ -6447,7 +6738,7 @@ async function createHttpTransport(server) {
|
|
|
6447
6738
|
`[mcp] Creating new transport: isInitReq=${isInitReq}, has_tenant=${!!mcpTenantContext}, has_client=${!!shopifyClient}`
|
|
6448
6739
|
);
|
|
6449
6740
|
transport = new StreamableHTTPServerTransport({
|
|
6450
|
-
sessionIdGenerator: () =>
|
|
6741
|
+
sessionIdGenerator: () => randomUUID2(),
|
|
6451
6742
|
onsessioninitialized: (newSessionId) => {
|
|
6452
6743
|
log.info(`[mcp] Session initialized: ${newSessionId.substring(0, 8)}...`);
|
|
6453
6744
|
transports.set(newSessionId, transport);
|
|
@@ -6500,7 +6791,7 @@ async function createHttpTransport(server) {
|
|
|
6500
6791
|
});
|
|
6501
6792
|
await server.connect(transport);
|
|
6502
6793
|
log.info("[mcp] Transport connected to server, handling request...");
|
|
6503
|
-
const tempContextKey = multiTenantContext?.correlationId ||
|
|
6794
|
+
const tempContextKey = multiTenantContext?.correlationId || randomUUID2();
|
|
6504
6795
|
try {
|
|
6505
6796
|
if (multiTenantContext) {
|
|
6506
6797
|
setFallbackContext(tempContextKey, multiTenantContext);
|
|
@@ -6639,7 +6930,7 @@ async function createHttpTransport(server) {
|
|
|
6639
6930
|
let shopifyClient;
|
|
6640
6931
|
let multiTenantContext;
|
|
6641
6932
|
if (isRemote && apiKeyService && clientPool && prisma2) {
|
|
6642
|
-
const { validateMcpBearerToken } = await import("./mcp-auth-
|
|
6933
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6643
6934
|
const authResult = await validateMcpBearerToken(
|
|
6644
6935
|
req.headers.authorization,
|
|
6645
6936
|
apiKeyService,
|
|
@@ -6804,7 +7095,7 @@ data: ${JSON.stringify({
|
|
|
6804
7095
|
const isRemote = isRemoteMode(config);
|
|
6805
7096
|
const prisma2 = isRemote ? getPrismaClient() : null;
|
|
6806
7097
|
if (isRemote && apiKeyService && prisma2) {
|
|
6807
|
-
const { validateMcpBearerToken } = await import("./mcp-auth-
|
|
7098
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6808
7099
|
const authResult = await validateMcpBearerToken(
|
|
6809
7100
|
req.headers.authorization,
|
|
6810
7101
|
apiKeyService,
|
|
@@ -6855,7 +7146,7 @@ data: ${JSON.stringify({
|
|
|
6855
7146
|
return;
|
|
6856
7147
|
}
|
|
6857
7148
|
if (body.method === "tools/list") {
|
|
6858
|
-
const { getRegisteredTools } = await import("./tools-
|
|
7149
|
+
const { getRegisteredTools } = await import("./tools-BCI3Z2AW.js");
|
|
6859
7150
|
const tools = getRegisteredTools();
|
|
6860
7151
|
const response = {
|
|
6861
7152
|
jsonrpc: "2.0",
|
|
@@ -6893,7 +7184,7 @@ data: ${JSON.stringify({
|
|
|
6893
7184
|
return;
|
|
6894
7185
|
}
|
|
6895
7186
|
if (body.method === "tools/call" && isRemote && apiKeyService && clientPool && prisma2) {
|
|
6896
|
-
const { validateMcpBearerToken } = await import("./mcp-auth-
|
|
7187
|
+
const { validateMcpBearerToken } = await import("./mcp-auth-CWOWKID3.js");
|
|
6897
7188
|
const authResult = await validateMcpBearerToken(
|
|
6898
7189
|
req.headers.authorization,
|
|
6899
7190
|
apiKeyService,
|
|
@@ -6950,7 +7241,7 @@ data: ${JSON.stringify({
|
|
|
6950
7241
|
),
|
|
6951
7242
|
correlationId
|
|
6952
7243
|
};
|
|
6953
|
-
const { getToolByName } = await import("./tools-
|
|
7244
|
+
const { getToolByName } = await import("./tools-BCI3Z2AW.js");
|
|
6954
7245
|
const params = body.params;
|
|
6955
7246
|
const rawToolName = params?.name;
|
|
6956
7247
|
const toolArgs = params?.arguments || {};
|
|
@@ -7143,6 +7434,31 @@ data: ${JSON.stringify({
|
|
|
7143
7434
|
});
|
|
7144
7435
|
}
|
|
7145
7436
|
});
|
|
7437
|
+
const faviconPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKklEQVR42u3NQQkAAAgEsItnQutYzRQ+hMH+S0+dikAgEAgEAoFAIPgSLM8edFuULS3fAAAAAElFTkSuQmCC";
|
|
7438
|
+
app.get("/favicon.ico", (_req, res) => {
|
|
7439
|
+
const faviconBuffer = Buffer.from(faviconPngBase64, "base64");
|
|
7440
|
+
res.setHeader("Content-Type", "image/png");
|
|
7441
|
+
res.setHeader("Content-Length", faviconBuffer.length.toString());
|
|
7442
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
7443
|
+
res.send(faviconBuffer);
|
|
7444
|
+
});
|
|
7445
|
+
app.get("/favicon.png", (_req, res) => {
|
|
7446
|
+
const faviconBuffer = Buffer.from(faviconPngBase64, "base64");
|
|
7447
|
+
res.setHeader("Content-Type", "image/png");
|
|
7448
|
+
res.setHeader("Content-Length", faviconBuffer.length.toString());
|
|
7449
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
7450
|
+
res.send(faviconBuffer);
|
|
7451
|
+
});
|
|
7452
|
+
app.get("/favicon.svg", (_req, res) => {
|
|
7453
|
+
const svgFavicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
7454
|
+
<rect width="32" height="32" rx="6" fill="#96BF48"/>
|
|
7455
|
+
<path d="M10 8h12l2 6v12a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V14l2-6z" fill="white" stroke="white" stroke-width="1"/>
|
|
7456
|
+
<path d="M12 8v-2a4 4 0 0 1 8 0v2" fill="none" stroke="#96BF48" stroke-width="2" stroke-linecap="round"/>
|
|
7457
|
+
</svg>`;
|
|
7458
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
7459
|
+
res.setHeader("Cache-Control", "public, max-age=86400");
|
|
7460
|
+
res.send(svgFavicon);
|
|
7461
|
+
});
|
|
7146
7462
|
const oauthDiscoveryService = createOAuthDiscoveryService(discoveryBaseUrl);
|
|
7147
7463
|
app.get("/.well-known/oauth-authorization-server", (_req, res) => {
|
|
7148
7464
|
const metadata = oauthDiscoveryService.getAuthorizationServerMetadata();
|
|
@@ -7184,18 +7500,43 @@ data: ${JSON.stringify({
|
|
|
7184
7500
|
const oauthAuthorizeService = createOAuthAuthorizeService(void 0, cryptoService || void 0);
|
|
7185
7501
|
app.get("/oauth/authorize", async (req, res) => {
|
|
7186
7502
|
try {
|
|
7503
|
+
const userAgent = req.headers["user-agent"] || "unknown";
|
|
7504
|
+
const origin = req.headers.origin || req.headers.referer || "none";
|
|
7505
|
+
const clientId = req.query.client_id;
|
|
7506
|
+
log.info("[OAuth] GET /oauth/authorize request", {
|
|
7507
|
+
client_id: clientId?.substring(0, 20),
|
|
7508
|
+
has_session_cookie: !!req.cookies?.session_id,
|
|
7509
|
+
origin: typeof origin === "string" ? origin.substring(0, 50) : "none",
|
|
7510
|
+
user_agent: userAgent.substring(0, 80),
|
|
7511
|
+
redirect_uri: req.query.redirect_uri?.substring(0, 80),
|
|
7512
|
+
// Check if this looks like a Claude request
|
|
7513
|
+
is_claude_origin: origin.includes("claude.ai") || origin.includes("anthropic.com")
|
|
7514
|
+
});
|
|
7187
7515
|
const sessionId = req.cookies?.session_id;
|
|
7188
7516
|
if (!sessionId) {
|
|
7517
|
+
log.info("[OAuth] No session cookie - redirecting to login", {
|
|
7518
|
+
client_id: clientId?.substring(0, 20),
|
|
7519
|
+
is_iframe: req.headers["sec-fetch-dest"] === "iframe"
|
|
7520
|
+
});
|
|
7189
7521
|
const returnUrl = `/oauth/authorize?${new URLSearchParams(req.query).toString()}`;
|
|
7190
7522
|
res.redirect(`/app/login?redirect=${encodeURIComponent(returnUrl)}`);
|
|
7191
7523
|
return;
|
|
7192
7524
|
}
|
|
7193
7525
|
const session = await sessionStore.get(sessionId);
|
|
7194
7526
|
if (!session || session.expiresAt < Date.now()) {
|
|
7527
|
+
log.info("[OAuth] Session expired or invalid - redirecting to login", {
|
|
7528
|
+
client_id: clientId?.substring(0, 20),
|
|
7529
|
+
session_exists: !!session,
|
|
7530
|
+
expired: session ? session.expiresAt < Date.now() : "N/A"
|
|
7531
|
+
});
|
|
7195
7532
|
const returnUrl = `/oauth/authorize?${new URLSearchParams(req.query).toString()}`;
|
|
7196
7533
|
res.redirect(`/app/login?redirect=${encodeURIComponent(returnUrl)}`);
|
|
7197
7534
|
return;
|
|
7198
7535
|
}
|
|
7536
|
+
log.info("[OAuth] Valid session found, proceeding with authorization", {
|
|
7537
|
+
client_id: clientId?.substring(0, 20),
|
|
7538
|
+
tenant_id: session.tenantId.substring(0, 8)
|
|
7539
|
+
});
|
|
7199
7540
|
const validationResult = await oauthAuthorizeService.validateAuthorizationRequest(
|
|
7200
7541
|
req.query
|
|
7201
7542
|
);
|