strapi-oauth-mcp-manager 0.1.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.
Files changed (37) hide show
  1. package/README.md +143 -0
  2. package/dist/_chunks/App-CjW3NftW.mjs +23 -0
  3. package/dist/_chunks/App-DsMhfKkM.js +23 -0
  4. package/dist/_chunks/en-B4KWt_jN.js +4 -0
  5. package/dist/_chunks/en-Byx4XI2L.mjs +4 -0
  6. package/dist/_chunks/index-B2ShbPnj.js +65 -0
  7. package/dist/_chunks/index-DzBaU9Fw.mjs +66 -0
  8. package/dist/admin/index.js +3 -0
  9. package/dist/admin/index.mjs +4 -0
  10. package/dist/admin/src/components/Initializer.d.ts +5 -0
  11. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  12. package/dist/admin/src/index.d.ts +10 -0
  13. package/dist/admin/src/pages/App.d.ts +2 -0
  14. package/dist/admin/src/pages/HomePage.d.ts +2 -0
  15. package/dist/admin/src/pluginId.d.ts +1 -0
  16. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  17. package/dist/server/index.js +800 -0
  18. package/dist/server/index.mjs +801 -0
  19. package/dist/server/src/bootstrap.d.ts +5 -0
  20. package/dist/server/src/config/index.d.ts +5 -0
  21. package/dist/server/src/content-types/index.d.ts +208 -0
  22. package/dist/server/src/controllers/index.d.ts +11 -0
  23. package/dist/server/src/controllers/oauth.d.ts +31 -0
  24. package/dist/server/src/destroy.d.ts +5 -0
  25. package/dist/server/src/index.d.ts +278 -0
  26. package/dist/server/src/middlewares/index.d.ts +6 -0
  27. package/dist/server/src/middlewares/mcp-oauth.d.ts +19 -0
  28. package/dist/server/src/pluginId.d.ts +1 -0
  29. package/dist/server/src/policies/index.d.ts +2 -0
  30. package/dist/server/src/register.d.ts +5 -0
  31. package/dist/server/src/routes/admin/index.d.ts +2 -0
  32. package/dist/server/src/routes/content-api/index.d.ts +10 -0
  33. package/dist/server/src/routes/index.d.ts +19 -0
  34. package/dist/server/src/services/endpoint.d.ts +37 -0
  35. package/dist/server/src/services/index.d.ts +22 -0
  36. package/dist/server/src/services/oauth.d.ts +32 -0
  37. package/package.json +88 -0
@@ -0,0 +1,801 @@
1
+ import { randomBytes } from "node:crypto";
2
+ const PLUGIN_ID = "strapi-oauth-mcp-manager";
3
+ const PLUGIN_UID$3 = `plugin::${PLUGIN_ID}`;
4
+ function extractBearerToken(authHeader) {
5
+ if (!authHeader?.startsWith("Bearer ")) {
6
+ return null;
7
+ }
8
+ return authHeader.slice(7);
9
+ }
10
+ function getBaseUrl$1(ctx, strapi) {
11
+ const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
12
+ const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
13
+ const serverUrl = strapi.config.get("server.url") || "http://localhost:1337";
14
+ return forwardedHost ? `${forwardedProto}://${forwardedHost}` : serverUrl;
15
+ }
16
+ function buildWwwAuthenticateHeader(ctx, strapi) {
17
+ const baseUrl = getBaseUrl$1(ctx, strapi);
18
+ const resourceMetadataUrl = `${baseUrl}/api/${PLUGIN_ID}/.well-known/oauth-protected-resource`;
19
+ return `Bearer resource_metadata="${resourceMetadataUrl}"`;
20
+ }
21
+ async function validateOAuthToken(token, strapi) {
22
+ try {
23
+ const tokenRecord = await strapi.documents(`${PLUGIN_UID$3}.mcp-oauth-token`).findFirst({
24
+ filters: { accessToken: token, revoked: false }
25
+ });
26
+ if (!tokenRecord) {
27
+ return { valid: false };
28
+ }
29
+ if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
30
+ return { valid: false, error: "Token expired" };
31
+ }
32
+ const client = await strapi.documents(`${PLUGIN_UID$3}.mcp-oauth-client`).findFirst({
33
+ filters: { clientId: tokenRecord.clientId }
34
+ });
35
+ if (!client) {
36
+ return { valid: false, error: "Client not found" };
37
+ }
38
+ return {
39
+ valid: true,
40
+ strapiApiToken: client.strapiApiToken
41
+ };
42
+ } catch (error) {
43
+ strapi.log.error(`[${PLUGIN_ID}] Error validating OAuth token`, { error });
44
+ return { valid: false, error: "Token validation failed" };
45
+ }
46
+ }
47
+ const mcpOauthMiddleware = (config2, { strapi }) => {
48
+ let endpointCache = [];
49
+ let lastCacheUpdate = 0;
50
+ const CACHE_TTL = 6e4;
51
+ async function refreshEndpointCache() {
52
+ const now = Date.now();
53
+ if (now - lastCacheUpdate > CACHE_TTL) {
54
+ try {
55
+ const endpoints = await strapi.documents(`${PLUGIN_UID$3}.mcp-endpoint`).findMany({
56
+ filters: { active: true }
57
+ });
58
+ endpointCache = endpoints.map((e) => e.path);
59
+ lastCacheUpdate = now;
60
+ } catch (error) {
61
+ strapi.log.error(`[${PLUGIN_ID}] Error refreshing endpoint cache`, { error });
62
+ }
63
+ }
64
+ }
65
+ function isProtectedPath(path) {
66
+ return endpointCache.some((endpoint) => path.includes(endpoint));
67
+ }
68
+ return async (ctx, next) => {
69
+ await refreshEndpointCache();
70
+ if (!isProtectedPath(ctx.path)) {
71
+ return next();
72
+ }
73
+ const authHeader = ctx.request.headers.authorization;
74
+ const token = extractBearerToken(authHeader);
75
+ if (!token) {
76
+ ctx.status = 401;
77
+ ctx.set("WWW-Authenticate", buildWwwAuthenticateHeader(ctx, strapi));
78
+ ctx.body = {
79
+ error: "Unauthorized",
80
+ message: "No authorization token provided"
81
+ };
82
+ return;
83
+ }
84
+ const oauthResult = await validateOAuthToken(token, strapi);
85
+ if (oauthResult.valid && oauthResult.strapiApiToken) {
86
+ ctx.state.strapiToken = oauthResult.strapiApiToken;
87
+ ctx.state.authMethod = "oauth";
88
+ return next();
89
+ }
90
+ ctx.state.strapiToken = token;
91
+ ctx.state.authMethod = "api-token";
92
+ return next();
93
+ };
94
+ };
95
+ const bootstrap = async ({ strapi }) => {
96
+ const middleware = mcpOauthMiddleware({}, { strapi });
97
+ strapi.server.use(middleware);
98
+ strapi.log.info(`[${PLUGIN_ID}] OAuth middleware registered`);
99
+ strapi.log.info(`[${PLUGIN_ID}] OAuth endpoints available at: /api/${PLUGIN_ID}/oauth/*`);
100
+ strapi.log.info(`[${PLUGIN_ID}] Discovery: /api/${PLUGIN_ID}/.well-known/oauth-authorization-server`);
101
+ };
102
+ const destroy = ({ strapi }) => {
103
+ };
104
+ const register = ({ strapi }) => {
105
+ };
106
+ const config = {
107
+ default: {},
108
+ validator() {
109
+ }
110
+ };
111
+ const kind$3 = "collectionType";
112
+ const collectionName$3 = "mcp_oauth_clients";
113
+ const info$3 = {
114
+ singularName: "mcp-oauth-client",
115
+ pluralName: "mcp-oauth-clients",
116
+ displayName: "MCP OAuth Client",
117
+ description: "OAuth 2.0 clients for MCP authentication"
118
+ };
119
+ const options$3 = {
120
+ draftAndPublish: false
121
+ };
122
+ const pluginOptions$3 = {
123
+ "content-manager": {
124
+ visible: true
125
+ },
126
+ "content-type-builder": {
127
+ visible: true
128
+ }
129
+ };
130
+ const attributes$3 = {
131
+ name: {
132
+ type: "string",
133
+ required: true
134
+ },
135
+ clientId: {
136
+ type: "string",
137
+ required: true,
138
+ unique: true
139
+ },
140
+ clientSecret: {
141
+ type: "string",
142
+ required: true,
143
+ "private": true
144
+ },
145
+ redirectUris: {
146
+ type: "json",
147
+ required: true
148
+ },
149
+ strapiApiToken: {
150
+ type: "string",
151
+ required: true,
152
+ "private": true
153
+ },
154
+ description: {
155
+ type: "text"
156
+ },
157
+ active: {
158
+ type: "boolean",
159
+ "default": true
160
+ }
161
+ };
162
+ const mcpOauthClient = {
163
+ kind: kind$3,
164
+ collectionName: collectionName$3,
165
+ info: info$3,
166
+ options: options$3,
167
+ pluginOptions: pluginOptions$3,
168
+ attributes: attributes$3
169
+ };
170
+ const kind$2 = "collectionType";
171
+ const collectionName$2 = "mcp_oauth_codes";
172
+ const info$2 = {
173
+ singularName: "mcp-oauth-code",
174
+ pluralName: "mcp-oauth-codes",
175
+ displayName: "MCP OAuth Code",
176
+ description: "OAuth 2.0 authorization codes"
177
+ };
178
+ const options$2 = {
179
+ draftAndPublish: false
180
+ };
181
+ const pluginOptions$2 = {
182
+ "content-manager": {
183
+ visible: false
184
+ },
185
+ "content-type-builder": {
186
+ visible: false
187
+ }
188
+ };
189
+ const attributes$2 = {
190
+ code: {
191
+ type: "string",
192
+ required: true,
193
+ unique: true
194
+ },
195
+ clientId: {
196
+ type: "string",
197
+ required: true
198
+ },
199
+ redirectUri: {
200
+ type: "string",
201
+ required: true
202
+ },
203
+ codeChallenge: {
204
+ type: "string"
205
+ },
206
+ codeChallengeMethod: {
207
+ type: "string"
208
+ },
209
+ expiresAt: {
210
+ type: "datetime",
211
+ required: true
212
+ },
213
+ used: {
214
+ type: "boolean",
215
+ "default": false
216
+ }
217
+ };
218
+ const mcpOauthCode = {
219
+ kind: kind$2,
220
+ collectionName: collectionName$2,
221
+ info: info$2,
222
+ options: options$2,
223
+ pluginOptions: pluginOptions$2,
224
+ attributes: attributes$2
225
+ };
226
+ const kind$1 = "collectionType";
227
+ const collectionName$1 = "mcp_oauth_tokens";
228
+ const info$1 = {
229
+ singularName: "mcp-oauth-token",
230
+ pluralName: "mcp-oauth-tokens",
231
+ displayName: "MCP OAuth Token",
232
+ description: "OAuth 2.0 access and refresh tokens"
233
+ };
234
+ const options$1 = {
235
+ draftAndPublish: false
236
+ };
237
+ const pluginOptions$1 = {
238
+ "content-manager": {
239
+ visible: false
240
+ },
241
+ "content-type-builder": {
242
+ visible: false
243
+ }
244
+ };
245
+ const attributes$1 = {
246
+ accessToken: {
247
+ type: "string",
248
+ required: true,
249
+ unique: true,
250
+ "private": true
251
+ },
252
+ refreshToken: {
253
+ type: "string",
254
+ required: true,
255
+ unique: true,
256
+ "private": true
257
+ },
258
+ clientId: {
259
+ type: "string",
260
+ required: true
261
+ },
262
+ expiresAt: {
263
+ type: "datetime",
264
+ required: true
265
+ },
266
+ refreshExpiresAt: {
267
+ type: "datetime",
268
+ required: true
269
+ },
270
+ revoked: {
271
+ type: "boolean",
272
+ "default": false
273
+ }
274
+ };
275
+ const mcpOauthToken = {
276
+ kind: kind$1,
277
+ collectionName: collectionName$1,
278
+ info: info$1,
279
+ options: options$1,
280
+ pluginOptions: pluginOptions$1,
281
+ attributes: attributes$1
282
+ };
283
+ const kind = "collectionType";
284
+ const collectionName = "mcp_endpoints";
285
+ const info = {
286
+ singularName: "mcp-endpoint",
287
+ pluralName: "mcp-endpoints",
288
+ displayName: "MCP Endpoint",
289
+ description: "Registered MCP endpoints protected by OAuth"
290
+ };
291
+ const options = {
292
+ draftAndPublish: false
293
+ };
294
+ const pluginOptions = {
295
+ "content-manager": {
296
+ visible: true
297
+ },
298
+ "content-type-builder": {
299
+ visible: true
300
+ }
301
+ };
302
+ const attributes = {
303
+ name: {
304
+ type: "string",
305
+ required: true
306
+ },
307
+ pluginId: {
308
+ type: "string",
309
+ required: true
310
+ },
311
+ path: {
312
+ type: "string",
313
+ required: true,
314
+ unique: true
315
+ },
316
+ description: {
317
+ type: "text"
318
+ },
319
+ active: {
320
+ type: "boolean",
321
+ "default": true
322
+ }
323
+ };
324
+ const mcpEndpoint = {
325
+ kind,
326
+ collectionName,
327
+ info,
328
+ options,
329
+ pluginOptions,
330
+ attributes
331
+ };
332
+ const contentTypes = {
333
+ "mcp-oauth-client": { schema: mcpOauthClient },
334
+ "mcp-oauth-code": { schema: mcpOauthCode },
335
+ "mcp-oauth-token": { schema: mcpOauthToken },
336
+ "mcp-endpoint": { schema: mcpEndpoint }
337
+ };
338
+ const PLUGIN_UID$2 = `plugin::${PLUGIN_ID}`;
339
+ function getBaseUrl(ctx, strapi) {
340
+ const forwardedProto = ctx.request.headers["x-forwarded-proto"] || ctx.protocol;
341
+ const forwardedHost = ctx.request.headers["x-forwarded-host"] || ctx.request.headers["host"];
342
+ const serverUrl = strapi.config.get("server.url") || "http://localhost:1337";
343
+ return forwardedHost ? `${forwardedProto}://${forwardedHost}` : serverUrl;
344
+ }
345
+ function matchRedirectUri(redirectUri, allowedPatterns) {
346
+ return allowedPatterns.some((pattern) => {
347
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
348
+ const regexPattern = escaped.replace(/\*/g, "[^/]*");
349
+ const regex = new RegExp("^" + regexPattern + "$");
350
+ return regex.test(redirectUri);
351
+ });
352
+ }
353
+ const oauthController = ({ strapi }) => ({
354
+ /**
355
+ * OAuth 2.0 Authorization Server Metadata (RFC 8414)
356
+ * GET /.well-known/oauth-authorization-server
357
+ */
358
+ async discovery(ctx) {
359
+ const baseUrl = getBaseUrl(ctx, strapi);
360
+ const pluginPath = `/api/${PLUGIN_ID}`;
361
+ const issuer = `${baseUrl}${pluginPath}`;
362
+ ctx.body = {
363
+ issuer,
364
+ authorization_endpoint: `${baseUrl}${pluginPath}/oauth/authorize`,
365
+ token_endpoint: `${baseUrl}${pluginPath}/oauth/token`,
366
+ response_types_supported: ["code"],
367
+ grant_types_supported: ["authorization_code", "refresh_token"],
368
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
369
+ code_challenge_methods_supported: ["S256"]
370
+ };
371
+ },
372
+ /**
373
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728)
374
+ * GET /.well-known/oauth-protected-resource
375
+ */
376
+ async protectedResource(ctx) {
377
+ const baseUrl = getBaseUrl(ctx, strapi);
378
+ const pluginPath = `/api/${PLUGIN_ID}`;
379
+ const authServer = `${baseUrl}${pluginPath}`;
380
+ const endpoints = await strapi.documents(`${PLUGIN_UID$2}.mcp-endpoint`).findMany({
381
+ filters: { active: true }
382
+ });
383
+ const resources = endpoints.map((e) => `${baseUrl}${e.path}`);
384
+ ctx.body = {
385
+ resource: resources.length === 1 ? resources[0] : resources,
386
+ authorization_servers: [authServer],
387
+ bearer_methods_supported: ["header"]
388
+ };
389
+ },
390
+ /**
391
+ * OAuth 2.0 Authorization Endpoint
392
+ * GET /oauth/authorize
393
+ */
394
+ async authorize(ctx) {
395
+ const { client_id, redirect_uri, response_type, state, code_challenge, code_challenge_method } = ctx.query;
396
+ if (!client_id) {
397
+ ctx.status = 400;
398
+ ctx.body = { error: "invalid_request", error_description: "client_id is required" };
399
+ return;
400
+ }
401
+ if (!redirect_uri) {
402
+ ctx.status = 400;
403
+ ctx.body = { error: "invalid_request", error_description: "redirect_uri is required" };
404
+ return;
405
+ }
406
+ if (response_type !== "code") {
407
+ ctx.status = 400;
408
+ ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
409
+ return;
410
+ }
411
+ const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
412
+ filters: { clientId: client_id, active: true }
413
+ });
414
+ if (!client) {
415
+ ctx.status = 400;
416
+ ctx.body = { error: "invalid_client", error_description: "Unknown client_id" };
417
+ return;
418
+ }
419
+ const allowedRedirects = client.redirectUris;
420
+ if (!matchRedirectUri(redirect_uri, allowedRedirects)) {
421
+ strapi.log.warn(`[${PLUGIN_ID}] Invalid redirect_uri: ${redirect_uri}`);
422
+ strapi.log.warn(`[${PLUGIN_ID}] Allowed patterns: ${allowedRedirects.join(", ")}`);
423
+ ctx.status = 400;
424
+ ctx.body = { error: "invalid_request", error_description: "Invalid redirect_uri" };
425
+ return;
426
+ }
427
+ const code = randomBytes(32).toString("hex");
428
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
429
+ await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-code`).create({
430
+ data: {
431
+ code,
432
+ clientId: client_id,
433
+ redirectUri: redirect_uri,
434
+ codeChallenge: code_challenge || null,
435
+ codeChallengeMethod: code_challenge_method || null,
436
+ expiresAt: expiresAt.toISOString(),
437
+ used: false
438
+ }
439
+ });
440
+ const redirectUrl = new URL(redirect_uri);
441
+ redirectUrl.searchParams.set("code", code);
442
+ if (state) {
443
+ redirectUrl.searchParams.set("state", state);
444
+ }
445
+ ctx.redirect(redirectUrl.toString());
446
+ },
447
+ /**
448
+ * OAuth 2.0 Token Endpoint
449
+ * POST /oauth/token
450
+ */
451
+ async token(ctx) {
452
+ const { grant_type, code, redirect_uri, client_id, client_secret, refresh_token } = ctx.request.body;
453
+ let authClientId = client_id;
454
+ let authClientSecret = client_secret;
455
+ const authHeader = ctx.request.headers.authorization;
456
+ if (authHeader && authHeader.startsWith("Basic ")) {
457
+ const credentials = Buffer.from(authHeader.slice(6), "base64").toString();
458
+ const [id, secret] = credentials.split(":");
459
+ authClientId = authClientId || id;
460
+ authClientSecret = authClientSecret || secret;
461
+ }
462
+ if (!authClientId) {
463
+ ctx.status = 401;
464
+ ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
465
+ return;
466
+ }
467
+ const client = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-client`).findFirst({
468
+ filters: { clientId: authClientId, active: true }
469
+ });
470
+ if (!client || authClientSecret && client.clientSecret !== authClientSecret) {
471
+ ctx.status = 401;
472
+ ctx.body = { error: "invalid_client", error_description: "Invalid client credentials" };
473
+ return;
474
+ }
475
+ if (grant_type === "authorization_code") {
476
+ await handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_uri);
477
+ } else if (grant_type === "refresh_token") {
478
+ await handleRefreshTokenGrant(ctx, strapi, client, refresh_token);
479
+ } else {
480
+ ctx.status = 400;
481
+ ctx.body = { error: "unsupported_grant_type", error_description: "Unsupported grant type" };
482
+ }
483
+ }
484
+ });
485
+ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_uri) {
486
+ if (!code) {
487
+ ctx.status = 400;
488
+ ctx.body = { error: "invalid_request", error_description: "code is required" };
489
+ return;
490
+ }
491
+ const authCode = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-code`).findFirst({
492
+ filters: { code, clientId: client.clientId, used: false }
493
+ });
494
+ if (!authCode) {
495
+ ctx.status = 400;
496
+ ctx.body = { error: "invalid_grant", error_description: "Invalid authorization code" };
497
+ return;
498
+ }
499
+ if (new Date(authCode.expiresAt) < /* @__PURE__ */ new Date()) {
500
+ ctx.status = 400;
501
+ ctx.body = { error: "invalid_grant", error_description: "Authorization code expired" };
502
+ return;
503
+ }
504
+ if (authCode.redirectUri !== redirect_uri) {
505
+ ctx.status = 400;
506
+ ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
507
+ return;
508
+ }
509
+ await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-code`).update({
510
+ documentId: authCode.documentId,
511
+ data: { used: true }
512
+ });
513
+ const accessToken = randomBytes(32).toString("hex");
514
+ const refreshToken = randomBytes(32).toString("hex");
515
+ const expiresIn = 3600;
516
+ const refreshExpiresIn = 30 * 24 * 3600;
517
+ await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).create({
518
+ data: {
519
+ accessToken,
520
+ refreshToken,
521
+ clientId: client.clientId,
522
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
523
+ refreshExpiresAt: new Date(Date.now() + refreshExpiresIn * 1e3).toISOString(),
524
+ revoked: false
525
+ }
526
+ });
527
+ ctx.body = {
528
+ access_token: accessToken,
529
+ token_type: "Bearer",
530
+ expires_in: expiresIn,
531
+ refresh_token: refreshToken
532
+ };
533
+ }
534
+ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
535
+ if (!refreshToken) {
536
+ ctx.status = 400;
537
+ ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
538
+ return;
539
+ }
540
+ const token = await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).findFirst({
541
+ filters: { refreshToken, clientId: client.clientId, revoked: false }
542
+ });
543
+ if (!token) {
544
+ ctx.status = 400;
545
+ ctx.body = { error: "invalid_grant", error_description: "Invalid refresh token" };
546
+ return;
547
+ }
548
+ if (new Date(token.refreshExpiresAt) < /* @__PURE__ */ new Date()) {
549
+ ctx.status = 400;
550
+ ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
551
+ return;
552
+ }
553
+ await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).update({
554
+ documentId: token.documentId,
555
+ data: { revoked: true }
556
+ });
557
+ const newAccessToken = randomBytes(32).toString("hex");
558
+ const newRefreshToken = randomBytes(32).toString("hex");
559
+ const expiresIn = 3600;
560
+ const refreshExpiresIn = 30 * 24 * 3600;
561
+ await strapi.documents(`${PLUGIN_UID$2}.mcp-oauth-token`).create({
562
+ data: {
563
+ accessToken: newAccessToken,
564
+ refreshToken: newRefreshToken,
565
+ clientId: client.clientId,
566
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
567
+ refreshExpiresAt: new Date(Date.now() + refreshExpiresIn * 1e3).toISOString(),
568
+ revoked: false
569
+ }
570
+ });
571
+ ctx.body = {
572
+ access_token: newAccessToken,
573
+ token_type: "Bearer",
574
+ expires_in: expiresIn,
575
+ refresh_token: newRefreshToken
576
+ };
577
+ }
578
+ const controllers = {
579
+ oauth: oauthController
580
+ };
581
+ const middlewares = {
582
+ "mcp-oauth": mcpOauthMiddleware
583
+ };
584
+ const policies = {};
585
+ const contentApi = [
586
+ // OAuth 2.0 Authorization Server Metadata (RFC 8414)
587
+ {
588
+ method: "GET",
589
+ path: "/.well-known/oauth-authorization-server",
590
+ handler: "oauth.discovery",
591
+ config: {
592
+ auth: false,
593
+ policies: []
594
+ }
595
+ },
596
+ // OAuth 2.0 Protected Resource Metadata (RFC 9728)
597
+ {
598
+ method: "GET",
599
+ path: "/.well-known/oauth-protected-resource",
600
+ handler: "oauth.protectedResource",
601
+ config: {
602
+ auth: false,
603
+ policies: []
604
+ }
605
+ },
606
+ // OAuth 2.0 Authorization Endpoint
607
+ {
608
+ method: "GET",
609
+ path: "/oauth/authorize",
610
+ handler: "oauth.authorize",
611
+ config: {
612
+ auth: false,
613
+ policies: []
614
+ }
615
+ },
616
+ // OAuth 2.0 Token Endpoint
617
+ {
618
+ method: "POST",
619
+ path: "/oauth/token",
620
+ handler: "oauth.token",
621
+ config: {
622
+ auth: false,
623
+ policies: []
624
+ }
625
+ }
626
+ ];
627
+ const admin = [];
628
+ const routes = {
629
+ "content-api": {
630
+ type: "content-api",
631
+ routes: [...contentApi]
632
+ },
633
+ admin: {
634
+ type: "admin",
635
+ routes: [...admin]
636
+ }
637
+ };
638
+ const PLUGIN_UID$1 = `plugin::${PLUGIN_ID}`;
639
+ const oauthService = ({ strapi }) => ({
640
+ /**
641
+ * Validate an OAuth access token
642
+ */
643
+ async validateToken(accessToken) {
644
+ try {
645
+ const tokenRecord = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findFirst({
646
+ filters: { accessToken, revoked: false }
647
+ });
648
+ if (!tokenRecord) {
649
+ return { valid: false };
650
+ }
651
+ if (new Date(tokenRecord.expiresAt) < /* @__PURE__ */ new Date()) {
652
+ return { valid: false, error: "Token expired" };
653
+ }
654
+ const client = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-client`).findFirst({
655
+ filters: { clientId: tokenRecord.clientId }
656
+ });
657
+ if (!client) {
658
+ return { valid: false, error: "Client not found" };
659
+ }
660
+ return {
661
+ valid: true,
662
+ strapiApiToken: client.strapiApiToken,
663
+ clientId: client.clientId
664
+ };
665
+ } catch (error) {
666
+ strapi.log.error(`[${PLUGIN_ID}] Error validating token`, { error });
667
+ return { valid: false, error: "Token validation failed" };
668
+ }
669
+ },
670
+ /**
671
+ * Revoke all tokens for a client
672
+ */
673
+ async revokeClientTokens(clientId) {
674
+ const tokens = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findMany({
675
+ filters: { clientId, revoked: false }
676
+ });
677
+ for (const token of tokens) {
678
+ await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).update({
679
+ documentId: token.documentId,
680
+ data: { revoked: true }
681
+ });
682
+ }
683
+ return tokens.length;
684
+ },
685
+ /**
686
+ * Clean up expired tokens and codes
687
+ */
688
+ async cleanupExpired() {
689
+ const now = (/* @__PURE__ */ new Date()).toISOString();
690
+ const expiredCodes = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).findMany({
691
+ filters: { expiresAt: { $lt: now } }
692
+ });
693
+ for (const code of expiredCodes) {
694
+ await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-code`).delete({
695
+ documentId: code.documentId
696
+ });
697
+ }
698
+ const expiredTokens = await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).findMany({
699
+ filters: { refreshExpiresAt: { $lt: now } }
700
+ });
701
+ for (const token of expiredTokens) {
702
+ await strapi.documents(`${PLUGIN_UID$1}.mcp-oauth-token`).delete({
703
+ documentId: token.documentId
704
+ });
705
+ }
706
+ return {
707
+ tokens: expiredTokens.length,
708
+ codes: expiredCodes.length
709
+ };
710
+ }
711
+ });
712
+ const PLUGIN_UID = `plugin::${PLUGIN_ID}`;
713
+ const endpointService = ({ strapi }) => ({
714
+ /**
715
+ * Register an MCP endpoint for OAuth protection
716
+ */
717
+ async register(endpoint) {
718
+ const existing = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
719
+ filters: { path: endpoint.path }
720
+ });
721
+ if (existing) {
722
+ return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
723
+ documentId: existing.documentId,
724
+ data: {
725
+ name: endpoint.name,
726
+ pluginId: endpoint.pluginId,
727
+ description: endpoint.description,
728
+ active: true
729
+ }
730
+ });
731
+ }
732
+ return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).create({
733
+ data: {
734
+ name: endpoint.name,
735
+ pluginId: endpoint.pluginId,
736
+ path: endpoint.path,
737
+ description: endpoint.description,
738
+ active: true
739
+ }
740
+ });
741
+ },
742
+ /**
743
+ * Unregister an MCP endpoint
744
+ */
745
+ async unregister(path) {
746
+ const endpoint = await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findFirst({
747
+ filters: { path }
748
+ });
749
+ if (!endpoint) {
750
+ return false;
751
+ }
752
+ await strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).update({
753
+ documentId: endpoint.documentId,
754
+ data: { active: false }
755
+ });
756
+ return true;
757
+ },
758
+ /**
759
+ * Get all registered endpoints
760
+ */
761
+ async getAll(activeOnly = true) {
762
+ const filters = activeOnly ? { active: true } : {};
763
+ return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
764
+ },
765
+ /**
766
+ * Get endpoints for a specific plugin
767
+ */
768
+ async getByPlugin(pluginId, activeOnly = true) {
769
+ const filters = { pluginId };
770
+ if (activeOnly) {
771
+ filters.active = true;
772
+ }
773
+ return strapi.documents(`${PLUGIN_UID}.mcp-endpoint`).findMany({ filters });
774
+ },
775
+ /**
776
+ * Check if a path is a protected endpoint
777
+ */
778
+ async isProtected(path) {
779
+ const endpoints = await this.getAll(true);
780
+ return endpoints.some((e) => path.includes(e.path));
781
+ }
782
+ });
783
+ const services = {
784
+ oauth: oauthService,
785
+ endpoint: endpointService
786
+ };
787
+ const index = {
788
+ register,
789
+ bootstrap,
790
+ destroy,
791
+ config,
792
+ controllers,
793
+ routes,
794
+ services,
795
+ contentTypes,
796
+ policies,
797
+ middlewares
798
+ };
799
+ export {
800
+ index as default
801
+ };