@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.
@@ -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
+ };