@anton.andrusenko/shopify-mcp-admin 2.1.2 → 2.2.1
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/README.md +400 -18
- package/dist/{chunk-QXLLD2A7.js → chunk-5QMYOO4B.js} +20 -4
- package/dist/{chunk-UXI33LQD.js → chunk-JU5IFCVJ.js} +1 -1
- package/dist/chunk-LMFNHULG.js +14035 -0
- package/dist/chunk-PQKNBYJN.js +254 -0
- package/dist/dashboard/assets/index-BfNrQS4y.js +120 -0
- package/dist/dashboard/assets/index-BfNrQS4y.js.map +1 -0
- package/dist/dashboard/assets/index-HBHxyHsM.css +1 -0
- package/dist/dashboard/index.html +5 -2
- package/dist/dashboard/mcp-icon.svg +36 -0
- package/dist/index.js +5702 -16274
- package/dist/mcp-auth-F25V6FEY.js +24 -0
- package/dist/{security-XP6MXK5B.js → security-44M6F2QU.js} +17 -3
- package/dist/{store-FM7HCQVW.js → store-JK2ZU6DR.js} +2 -2
- package/dist/tools-HVUCP53D.js +82 -0
- package/package.json +4 -2
- package/dist/chunk-DCQTHXKI.js +0 -124
- package/dist/dashboard/assets/index-BSh6M640.js +0 -107
- package/dist/dashboard/assets/index-BSh6M640.js.map +0 -1
- package/dist/dashboard/assets/index-DLoVESj2.css +0 -1
- package/dist/mcp-auth-2WVQELV5.js +0 -16
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-5QMYOO4B.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/mcp-auth.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
var ACCESS_TOKEN_PREFIX = "mcp_access_";
|
|
8
|
+
var MCP_AUTH_ERROR_CODES = {
|
|
9
|
+
AUTH_REQUIRED: -32001,
|
|
10
|
+
// Missing authentication
|
|
11
|
+
AUTH_INVALID: -32002,
|
|
12
|
+
// Invalid API key format or key
|
|
13
|
+
AUTH_REVOKED: -32003,
|
|
14
|
+
// API key has been revoked
|
|
15
|
+
AUTH_FORBIDDEN: -32004
|
|
16
|
+
// Tenant suspended or no access
|
|
17
|
+
};
|
|
18
|
+
function createJsonRpcError(code, message, hint) {
|
|
19
|
+
const error = {
|
|
20
|
+
jsonrpc: "2.0",
|
|
21
|
+
error: {
|
|
22
|
+
code,
|
|
23
|
+
message
|
|
24
|
+
},
|
|
25
|
+
id: null
|
|
26
|
+
};
|
|
27
|
+
if (hint) {
|
|
28
|
+
error.error.data = { hint };
|
|
29
|
+
}
|
|
30
|
+
return error;
|
|
31
|
+
}
|
|
32
|
+
function parseBearerToken(authHeader) {
|
|
33
|
+
if (!authHeader) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const parts = authHeader.split(" ");
|
|
37
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const token = parts[1];
|
|
41
|
+
if (!token || token.trim() === "") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
async function validateMcpApiKey(authHeader, apiKeyService, prisma) {
|
|
47
|
+
const token = parseBearerToken(authHeader);
|
|
48
|
+
if (!token) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
error: "Authentication required",
|
|
52
|
+
httpStatus: 401
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const validationResult = await apiKeyService.validate(token);
|
|
56
|
+
if (!validationResult.valid) {
|
|
57
|
+
const isRevoked = validationResult.error?.includes("revoked");
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
error: validationResult.error || "Invalid API key",
|
|
61
|
+
httpStatus: isRevoked ? 403 : 401
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (validationResult.apiKeyId) {
|
|
65
|
+
if (typeof apiKeyService.recordUsage === "function") {
|
|
66
|
+
apiKeyService.recordUsage(
|
|
67
|
+
validationResult.apiKeyId
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const tenant = validationResult.tenant;
|
|
72
|
+
const tenantShops = await prisma.tenantShop.findMany({
|
|
73
|
+
where: {
|
|
74
|
+
tenantId: tenant.tenantId,
|
|
75
|
+
uninstalledAt: null
|
|
76
|
+
// Only active shops
|
|
77
|
+
},
|
|
78
|
+
select: {
|
|
79
|
+
shopDomain: true,
|
|
80
|
+
scopes: true
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const allowedShops = tenantShops.map((shop) => shop.shopDomain);
|
|
84
|
+
let defaultShop = validationResult.shop;
|
|
85
|
+
if (!defaultShop && tenantShops.length === 1) {
|
|
86
|
+
defaultShop = {
|
|
87
|
+
domain: tenantShops[0].shopDomain,
|
|
88
|
+
scopes: tenantShops[0].scopes
|
|
89
|
+
};
|
|
90
|
+
log.debug("[mcp-auth] API key: no defaultShopId set, falling back to the only connected shop");
|
|
91
|
+
}
|
|
92
|
+
const mcpTenantContext = {
|
|
93
|
+
tenantId: tenant.tenantId,
|
|
94
|
+
email: tenant.email,
|
|
95
|
+
apiKeyId: validationResult.apiKeyId,
|
|
96
|
+
defaultShop,
|
|
97
|
+
allowedShops
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
valid: true,
|
|
101
|
+
tenant: mcpTenantContext
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function hashToken(token) {
|
|
105
|
+
return createHash("sha256").update(token).digest("hex");
|
|
106
|
+
}
|
|
107
|
+
function isOAuthAccessToken(token) {
|
|
108
|
+
return token.startsWith(ACCESS_TOKEN_PREFIX);
|
|
109
|
+
}
|
|
110
|
+
async function validateOAuthAccessToken(token, prisma) {
|
|
111
|
+
const tokenHash = hashToken(token);
|
|
112
|
+
const accessToken = await prisma.oAuthAccessToken.findUnique({
|
|
113
|
+
where: { tokenHash },
|
|
114
|
+
include: {
|
|
115
|
+
tenant: true,
|
|
116
|
+
shop: true,
|
|
117
|
+
client: true
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (!accessToken) {
|
|
121
|
+
log.debug("[mcp-auth] OAuth access token not found");
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
error: "Invalid access token",
|
|
125
|
+
httpStatus: 401
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (/* @__PURE__ */ new Date() > accessToken.expiresAt) {
|
|
129
|
+
log.debug("[mcp-auth] OAuth access token expired");
|
|
130
|
+
return {
|
|
131
|
+
valid: false,
|
|
132
|
+
error: "Access token expired",
|
|
133
|
+
httpStatus: 401
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (accessToken.tenant.status !== "ACTIVE") {
|
|
137
|
+
log.debug("[mcp-auth] Tenant inactive for OAuth token");
|
|
138
|
+
return {
|
|
139
|
+
valid: false,
|
|
140
|
+
error: "Tenant account is suspended",
|
|
141
|
+
httpStatus: 403
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const tenantShops = await prisma.tenantShop.findMany({
|
|
145
|
+
where: {
|
|
146
|
+
tenantId: accessToken.tenantId,
|
|
147
|
+
uninstalledAt: null
|
|
148
|
+
// Only active shops
|
|
149
|
+
},
|
|
150
|
+
select: {
|
|
151
|
+
shopDomain: true,
|
|
152
|
+
scopes: true
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const allowedShops = tenantShops.map((shop) => shop.shopDomain);
|
|
156
|
+
let defaultShop;
|
|
157
|
+
if (accessToken.shop) {
|
|
158
|
+
defaultShop = {
|
|
159
|
+
domain: accessToken.shop.shopDomain,
|
|
160
|
+
scopes: accessToken.shop.scopes || []
|
|
161
|
+
};
|
|
162
|
+
} else if (tenantShops.length > 0) {
|
|
163
|
+
defaultShop = {
|
|
164
|
+
domain: tenantShops[0].shopDomain,
|
|
165
|
+
scopes: tenantShops[0].scopes || []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const mcpTenantContext = {
|
|
169
|
+
tenantId: accessToken.tenantId,
|
|
170
|
+
email: accessToken.tenant.email,
|
|
171
|
+
// No apiKeyId for OAuth tokens
|
|
172
|
+
defaultShop,
|
|
173
|
+
allowedShops
|
|
174
|
+
};
|
|
175
|
+
log.debug(
|
|
176
|
+
`[mcp-auth] OAuth access token validated for tenant: ${accessToken.tenantId.substring(0, 8)}...`
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
valid: true,
|
|
180
|
+
tenant: mcpTenantContext
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async function validateMcpBearerToken(authHeader, apiKeyService, prisma) {
|
|
184
|
+
const token = parseBearerToken(authHeader);
|
|
185
|
+
if (!token) {
|
|
186
|
+
return {
|
|
187
|
+
valid: false,
|
|
188
|
+
error: "Authentication required",
|
|
189
|
+
httpStatus: 401
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (isOAuthAccessToken(token)) {
|
|
193
|
+
log.debug("[mcp-auth] Token identified as OAuth access token");
|
|
194
|
+
return validateOAuthAccessToken(token, prisma);
|
|
195
|
+
}
|
|
196
|
+
log.debug("[mcp-auth] Token identified as API key");
|
|
197
|
+
return validateMcpApiKey(authHeader, apiKeyService, prisma);
|
|
198
|
+
}
|
|
199
|
+
function getWwwAuthenticateHeader(baseUrl, error, errorDescription) {
|
|
200
|
+
let header = `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`;
|
|
201
|
+
if (error) {
|
|
202
|
+
header += `, error="${error}"`;
|
|
203
|
+
}
|
|
204
|
+
if (errorDescription) {
|
|
205
|
+
header += `, error_description="${errorDescription}"`;
|
|
206
|
+
}
|
|
207
|
+
return header;
|
|
208
|
+
}
|
|
209
|
+
function createMcpAuthMiddleware(options) {
|
|
210
|
+
const { apiKeyService, prisma, isRemote, baseUrl } = options;
|
|
211
|
+
return async (req, res, next) => {
|
|
212
|
+
if (!isRemote) {
|
|
213
|
+
log.debug("[mcp-auth] Local mode: skipping API key validation");
|
|
214
|
+
next();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const authResult = await validateMcpApiKey(req.headers.authorization, apiKeyService, prisma);
|
|
218
|
+
if (!authResult.valid) {
|
|
219
|
+
log.debug(`[mcp-auth] Auth failed: ${authResult.error}`);
|
|
220
|
+
let errorCode;
|
|
221
|
+
let hint;
|
|
222
|
+
if (authResult.httpStatus === 401) {
|
|
223
|
+
errorCode = authResult.error === "Authentication required" ? MCP_AUTH_ERROR_CODES.AUTH_REQUIRED : MCP_AUTH_ERROR_CODES.AUTH_INVALID;
|
|
224
|
+
hint = "Include Authorization: Bearer sk_live_xxx header";
|
|
225
|
+
if (baseUrl) {
|
|
226
|
+
res.setHeader("WWW-Authenticate", getWwwAuthenticateHeader(baseUrl));
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
errorCode = authResult.error?.includes("revoked") ? MCP_AUTH_ERROR_CODES.AUTH_REVOKED : MCP_AUTH_ERROR_CODES.AUTH_FORBIDDEN;
|
|
230
|
+
hint = "API key has been revoked or tenant is inactive";
|
|
231
|
+
}
|
|
232
|
+
res.status(authResult.httpStatus || 401).json(createJsonRpcError(errorCode, authResult.error || "Authentication failed", hint));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
req.mcpTenantContext = authResult.tenant;
|
|
236
|
+
log.debug(
|
|
237
|
+
`[mcp-auth] Auth successful for tenant: ${authResult.tenant?.tenantId?.substring(0, 8)}...`
|
|
238
|
+
);
|
|
239
|
+
next();
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
var MCP_AUTH_ERRORS = MCP_AUTH_ERROR_CODES;
|
|
243
|
+
|
|
244
|
+
export {
|
|
245
|
+
createJsonRpcError,
|
|
246
|
+
parseBearerToken,
|
|
247
|
+
validateMcpApiKey,
|
|
248
|
+
isOAuthAccessToken,
|
|
249
|
+
validateOAuthAccessToken,
|
|
250
|
+
validateMcpBearerToken,
|
|
251
|
+
getWwwAuthenticateHeader,
|
|
252
|
+
createMcpAuthMiddleware,
|
|
253
|
+
MCP_AUTH_ERRORS
|
|
254
|
+
};
|