yt-transcript-strapi-plugin 0.0.26 → 0.0.27-oauth.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.
@@ -868,17 +868,17 @@ const config = {
868
868
  }
869
869
  }
870
870
  };
871
- const kind = "collectionType";
872
- const collectionName = "transcript";
873
- const info = {
871
+ const kind$3 = "collectionType";
872
+ const collectionName$3 = "transcript";
873
+ const info$3 = {
874
874
  singularName: "transcript",
875
875
  pluralName: "transcripts",
876
876
  displayName: "Transcript"
877
877
  };
878
- const options = {
878
+ const options$3 = {
879
879
  draftAndPublish: false
880
880
  };
881
- const pluginOptions = {
881
+ const pluginOptions$3 = {
882
882
  "content-manager": {
883
883
  visible: true
884
884
  },
@@ -886,7 +886,7 @@ const pluginOptions = {
886
886
  visible: true
887
887
  }
888
888
  };
889
- const attributes = {
889
+ const attributes$3 = {
890
890
  title: {
891
891
  type: "string"
892
892
  },
@@ -900,6 +900,174 @@ const attributes = {
900
900
  type: "json"
901
901
  }
902
902
  };
903
+ const schema$3 = {
904
+ kind: kind$3,
905
+ collectionName: collectionName$3,
906
+ info: info$3,
907
+ options: options$3,
908
+ pluginOptions: pluginOptions$3,
909
+ attributes: attributes$3
910
+ };
911
+ const transcript = {
912
+ schema: schema$3
913
+ };
914
+ const kind$2 = "collectionType";
915
+ const collectionName$2 = "oauth_clients";
916
+ const info$2 = {
917
+ singularName: "oauth-client",
918
+ pluralName: "oauth-clients",
919
+ displayName: "OAuth Client",
920
+ description: "OAuth 2.0 clients for MCP authentication"
921
+ };
922
+ const options$2 = {
923
+ draftAndPublish: false
924
+ };
925
+ const pluginOptions$2 = {
926
+ "content-manager": {
927
+ visible: true
928
+ },
929
+ "content-type-builder": {
930
+ visible: false
931
+ }
932
+ };
933
+ const attributes$2 = {
934
+ clientId: {
935
+ type: "string",
936
+ required: true,
937
+ unique: true
938
+ },
939
+ clientSecret: {
940
+ type: "string",
941
+ required: true,
942
+ "private": true
943
+ },
944
+ name: {
945
+ type: "string",
946
+ required: true
947
+ },
948
+ redirectUris: {
949
+ type: "json",
950
+ required: true
951
+ },
952
+ strapiApiToken: {
953
+ type: "string",
954
+ required: true,
955
+ "private": true
956
+ },
957
+ active: {
958
+ type: "boolean",
959
+ "default": true
960
+ }
961
+ };
962
+ const schema$2 = {
963
+ kind: kind$2,
964
+ collectionName: collectionName$2,
965
+ info: info$2,
966
+ options: options$2,
967
+ pluginOptions: pluginOptions$2,
968
+ attributes: attributes$2
969
+ };
970
+ const oauthClient = {
971
+ schema: schema$2
972
+ };
973
+ const kind$1 = "collectionType";
974
+ const collectionName$1 = "oauth_codes";
975
+ const info$1 = {
976
+ singularName: "oauth-code",
977
+ pluralName: "oauth-codes",
978
+ displayName: "OAuth Code",
979
+ description: "OAuth 2.0 authorization codes (temporary)"
980
+ };
981
+ const options$1 = {
982
+ draftAndPublish: false
983
+ };
984
+ const pluginOptions$1 = {
985
+ "content-manager": {
986
+ visible: false
987
+ },
988
+ "content-type-builder": {
989
+ visible: false
990
+ }
991
+ };
992
+ const attributes$1 = {
993
+ code: {
994
+ type: "string",
995
+ required: true,
996
+ unique: true
997
+ },
998
+ clientId: {
999
+ type: "string",
1000
+ required: true
1001
+ },
1002
+ redirectUri: {
1003
+ type: "string",
1004
+ required: true
1005
+ },
1006
+ expiresAt: {
1007
+ type: "datetime",
1008
+ required: true
1009
+ },
1010
+ used: {
1011
+ type: "boolean",
1012
+ "default": false
1013
+ }
1014
+ };
1015
+ const schema$1 = {
1016
+ kind: kind$1,
1017
+ collectionName: collectionName$1,
1018
+ info: info$1,
1019
+ options: options$1,
1020
+ pluginOptions: pluginOptions$1,
1021
+ attributes: attributes$1
1022
+ };
1023
+ const oauthCode = {
1024
+ schema: schema$1
1025
+ };
1026
+ const kind = "collectionType";
1027
+ const collectionName = "oauth_tokens";
1028
+ const info = {
1029
+ singularName: "oauth-token",
1030
+ pluralName: "oauth-tokens",
1031
+ displayName: "OAuth Token",
1032
+ description: "OAuth 2.0 access tokens"
1033
+ };
1034
+ const options = {
1035
+ draftAndPublish: false
1036
+ };
1037
+ const pluginOptions = {
1038
+ "content-manager": {
1039
+ visible: false
1040
+ },
1041
+ "content-type-builder": {
1042
+ visible: false
1043
+ }
1044
+ };
1045
+ const attributes = {
1046
+ accessToken: {
1047
+ type: "string",
1048
+ required: true,
1049
+ unique: true
1050
+ },
1051
+ refreshToken: {
1052
+ type: "string",
1053
+ unique: true
1054
+ },
1055
+ clientId: {
1056
+ type: "string",
1057
+ required: true
1058
+ },
1059
+ expiresAt: {
1060
+ type: "datetime",
1061
+ required: true
1062
+ },
1063
+ refreshExpiresAt: {
1064
+ type: "datetime"
1065
+ },
1066
+ revoked: {
1067
+ type: "boolean",
1068
+ "default": false
1069
+ }
1070
+ };
903
1071
  const schema = {
904
1072
  kind,
905
1073
  collectionName,
@@ -908,11 +1076,14 @@ const schema = {
908
1076
  pluginOptions,
909
1077
  attributes
910
1078
  };
911
- const transcript = {
1079
+ const oauthToken = {
912
1080
  schema
913
1081
  };
914
1082
  const contentTypes = {
915
- transcript
1083
+ transcript,
1084
+ "oauth-client": oauthClient,
1085
+ "oauth-code": oauthCode,
1086
+ "oauth-token": oauthToken
916
1087
  };
917
1088
  const controller = ({ strapi }) => ({
918
1089
  async getTranscript(ctx) {
@@ -935,9 +1106,29 @@ const controller = ({ strapi }) => ({
935
1106
  ctx.body = { data: transcript2 };
936
1107
  }
937
1108
  });
1109
+ function extractBearerToken(authHeader) {
1110
+ if (!authHeader?.startsWith("Bearer ")) {
1111
+ return null;
1112
+ }
1113
+ return authHeader.slice(7);
1114
+ }
1115
+ async function authenticateRequest(ctx, plugin) {
1116
+ const authHeader = ctx.request.headers.authorization;
1117
+ const token = extractBearerToken(authHeader);
1118
+ if (!token) {
1119
+ return { authenticated: false, error: "No authorization token provided" };
1120
+ }
1121
+ const oauthService2 = plugin.service("oauth");
1122
+ const oauthResult = await oauthService2.validateAccessToken(token);
1123
+ if (oauthResult.valid) {
1124
+ return { authenticated: true, strapiToken: oauthResult.strapiApiToken };
1125
+ }
1126
+ return { authenticated: true, strapiToken: token };
1127
+ }
938
1128
  const mcpController = ({ strapi }) => ({
939
1129
  /**
940
1130
  * Handle MCP requests (POST, GET, DELETE)
1131
+ * Supports dual authentication: OAuth tokens and Strapi API tokens
941
1132
  * Creates a new server+transport per session for proper isolation
942
1133
  */
943
1134
  async handle(ctx) {
@@ -950,6 +1141,15 @@ const mcpController = ({ strapi }) => ({
950
1141
  };
951
1142
  return;
952
1143
  }
1144
+ const authResult = await authenticateRequest(ctx, plugin);
1145
+ if (!authResult.authenticated) {
1146
+ ctx.status = 401;
1147
+ ctx.body = {
1148
+ error: "Unauthorized",
1149
+ message: authResult.error || "Authentication required"
1150
+ };
1151
+ return;
1152
+ }
953
1153
  try {
954
1154
  const sessionId = ctx.request.headers["mcp-session-id"] || node_crypto.randomUUID();
955
1155
  let session = plugin.sessions.get(sessionId);
@@ -959,7 +1159,7 @@ const mcpController = ({ strapi }) => ({
959
1159
  sessionIdGenerator: () => sessionId
960
1160
  });
961
1161
  await server.connect(transport);
962
- session = { server, transport, createdAt: Date.now() };
1162
+ session = { server, transport, createdAt: Date.now(), strapiToken: authResult.strapiToken };
963
1163
  plugin.sessions.set(sessionId, session);
964
1164
  strapi.log.debug(`[yt-transcript-mcp] New session created: ${sessionId}`);
965
1165
  }
@@ -981,9 +1181,213 @@ const mcpController = ({ strapi }) => ({
981
1181
  }
982
1182
  }
983
1183
  });
1184
+ const PLUGIN_ID$1 = "plugin::yt-transcript-strapi-plugin";
1185
+ const oauthController = ({ strapi }) => ({
1186
+ /**
1187
+ * OAuth 2.0 Authorization Server Metadata (RFC 8414)
1188
+ * GET /.well-known/oauth-authorization-server
1189
+ */
1190
+ async discovery(ctx) {
1191
+ const baseUrl = strapi.config.get("server.url") || `${ctx.protocol}://${ctx.host}`;
1192
+ const pluginPath = "/api/yt-transcript-strapi-plugin";
1193
+ ctx.body = {
1194
+ issuer: baseUrl,
1195
+ authorization_endpoint: `${baseUrl}${pluginPath}/oauth/authorize`,
1196
+ token_endpoint: `${baseUrl}${pluginPath}/oauth/token`,
1197
+ response_types_supported: ["code"],
1198
+ grant_types_supported: ["authorization_code", "refresh_token"],
1199
+ token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
1200
+ code_challenge_methods_supported: ["S256"]
1201
+ };
1202
+ },
1203
+ /**
1204
+ * OAuth 2.0 Authorization Endpoint
1205
+ * GET /oauth/authorize
1206
+ */
1207
+ async authorize(ctx) {
1208
+ const { client_id, redirect_uri, response_type, state, scope } = ctx.query;
1209
+ if (!client_id) {
1210
+ ctx.status = 400;
1211
+ ctx.body = { error: "invalid_request", error_description: "client_id is required" };
1212
+ return;
1213
+ }
1214
+ if (!redirect_uri) {
1215
+ ctx.status = 400;
1216
+ ctx.body = { error: "invalid_request", error_description: "redirect_uri is required" };
1217
+ return;
1218
+ }
1219
+ if (response_type !== "code") {
1220
+ ctx.status = 400;
1221
+ ctx.body = { error: "unsupported_response_type", error_description: "Only code response type is supported" };
1222
+ return;
1223
+ }
1224
+ const client = await strapi.documents(`${PLUGIN_ID$1}.oauth-client`).findFirst({
1225
+ filters: { clientId: client_id, active: true }
1226
+ });
1227
+ if (!client) {
1228
+ ctx.status = 400;
1229
+ ctx.body = { error: "invalid_client", error_description: "Unknown client_id" };
1230
+ return;
1231
+ }
1232
+ const allowedRedirects = client.redirectUris;
1233
+ if (!allowedRedirects.includes(redirect_uri)) {
1234
+ ctx.status = 400;
1235
+ ctx.body = { error: "invalid_request", error_description: "Invalid redirect_uri" };
1236
+ return;
1237
+ }
1238
+ const code = node_crypto.randomBytes(32).toString("hex");
1239
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
1240
+ await strapi.documents(`${PLUGIN_ID$1}.oauth-code`).create({
1241
+ data: {
1242
+ code,
1243
+ clientId: client_id,
1244
+ redirectUri: redirect_uri,
1245
+ expiresAt: expiresAt.toISOString(),
1246
+ used: false
1247
+ }
1248
+ });
1249
+ const redirectUrl = new URL(redirect_uri);
1250
+ redirectUrl.searchParams.set("code", code);
1251
+ if (state) {
1252
+ redirectUrl.searchParams.set("state", state);
1253
+ }
1254
+ ctx.redirect(redirectUrl.toString());
1255
+ },
1256
+ /**
1257
+ * OAuth 2.0 Token Endpoint
1258
+ * POST /oauth/token
1259
+ */
1260
+ async token(ctx) {
1261
+ const { grant_type, code, redirect_uri, client_id, client_secret, refresh_token } = ctx.request.body;
1262
+ let authClientId = client_id;
1263
+ let authClientSecret = client_secret;
1264
+ const authHeader = ctx.request.headers.authorization;
1265
+ if (authHeader && authHeader.startsWith("Basic ")) {
1266
+ const credentials = Buffer.from(authHeader.slice(6), "base64").toString();
1267
+ const [id, secret] = credentials.split(":");
1268
+ authClientId = authClientId || id;
1269
+ authClientSecret = authClientSecret || secret;
1270
+ }
1271
+ if (!authClientId || !authClientSecret) {
1272
+ ctx.status = 401;
1273
+ ctx.body = { error: "invalid_client", error_description: "Client authentication required" };
1274
+ return;
1275
+ }
1276
+ const client = await strapi.documents(`${PLUGIN_ID$1}.oauth-client`).findFirst({
1277
+ filters: { clientId: authClientId, active: true }
1278
+ });
1279
+ if (!client || client.clientSecret !== authClientSecret) {
1280
+ ctx.status = 401;
1281
+ ctx.body = { error: "invalid_client", error_description: "Invalid client credentials" };
1282
+ return;
1283
+ }
1284
+ if (grant_type === "authorization_code") {
1285
+ return await handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_uri);
1286
+ } else if (grant_type === "refresh_token") {
1287
+ return await handleRefreshTokenGrant(ctx, strapi, client, refresh_token);
1288
+ } else {
1289
+ ctx.status = 400;
1290
+ ctx.body = { error: "unsupported_grant_type", error_description: "Unsupported grant type" };
1291
+ }
1292
+ }
1293
+ });
1294
+ async function handleAuthorizationCodeGrant(ctx, strapi, client, code, redirect_uri) {
1295
+ if (!code) {
1296
+ ctx.status = 400;
1297
+ ctx.body = { error: "invalid_request", error_description: "code is required" };
1298
+ return;
1299
+ }
1300
+ const authCode = await strapi.documents(`${PLUGIN_ID$1}.oauth-code`).findFirst({
1301
+ filters: { code, clientId: client.clientId, used: false }
1302
+ });
1303
+ if (!authCode) {
1304
+ ctx.status = 400;
1305
+ ctx.body = { error: "invalid_grant", error_description: "Invalid authorization code" };
1306
+ return;
1307
+ }
1308
+ if (new Date(authCode.expiresAt) < /* @__PURE__ */ new Date()) {
1309
+ ctx.status = 400;
1310
+ ctx.body = { error: "invalid_grant", error_description: "Authorization code expired" };
1311
+ return;
1312
+ }
1313
+ if (authCode.redirectUri !== redirect_uri) {
1314
+ ctx.status = 400;
1315
+ ctx.body = { error: "invalid_grant", error_description: "redirect_uri mismatch" };
1316
+ return;
1317
+ }
1318
+ await strapi.documents(`${PLUGIN_ID$1}.oauth-code`).update({
1319
+ documentId: authCode.documentId,
1320
+ data: { used: true }
1321
+ });
1322
+ const accessToken = node_crypto.randomBytes(32).toString("hex");
1323
+ const refreshToken = node_crypto.randomBytes(32).toString("hex");
1324
+ const expiresIn = 3600;
1325
+ const refreshExpiresIn = 30 * 24 * 3600;
1326
+ await strapi.documents(`${PLUGIN_ID$1}.oauth-token`).create({
1327
+ data: {
1328
+ accessToken,
1329
+ refreshToken,
1330
+ clientId: client.clientId,
1331
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
1332
+ refreshExpiresAt: new Date(Date.now() + refreshExpiresIn * 1e3).toISOString(),
1333
+ revoked: false
1334
+ }
1335
+ });
1336
+ ctx.body = {
1337
+ access_token: accessToken,
1338
+ token_type: "Bearer",
1339
+ expires_in: expiresIn,
1340
+ refresh_token: refreshToken
1341
+ };
1342
+ }
1343
+ async function handleRefreshTokenGrant(ctx, strapi, client, refreshToken) {
1344
+ if (!refreshToken) {
1345
+ ctx.status = 400;
1346
+ ctx.body = { error: "invalid_request", error_description: "refresh_token is required" };
1347
+ return;
1348
+ }
1349
+ const token = await strapi.documents(`${PLUGIN_ID$1}.oauth-token`).findFirst({
1350
+ filters: { refreshToken, clientId: client.clientId, revoked: false }
1351
+ });
1352
+ if (!token) {
1353
+ ctx.status = 400;
1354
+ ctx.body = { error: "invalid_grant", error_description: "Invalid refresh token" };
1355
+ return;
1356
+ }
1357
+ if (new Date(token.refreshExpiresAt) < /* @__PURE__ */ new Date()) {
1358
+ ctx.status = 400;
1359
+ ctx.body = { error: "invalid_grant", error_description: "Refresh token expired" };
1360
+ return;
1361
+ }
1362
+ await strapi.documents(`${PLUGIN_ID$1}.oauth-token`).update({
1363
+ documentId: token.documentId,
1364
+ data: { revoked: true }
1365
+ });
1366
+ const newAccessToken = node_crypto.randomBytes(32).toString("hex");
1367
+ const newRefreshToken = node_crypto.randomBytes(32).toString("hex");
1368
+ const expiresIn = 3600;
1369
+ const refreshExpiresIn = 30 * 24 * 3600;
1370
+ await strapi.documents(`${PLUGIN_ID$1}.oauth-token`).create({
1371
+ data: {
1372
+ accessToken: newAccessToken,
1373
+ refreshToken: newRefreshToken,
1374
+ clientId: client.clientId,
1375
+ expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString(),
1376
+ refreshExpiresAt: new Date(Date.now() + refreshExpiresIn * 1e3).toISOString(),
1377
+ revoked: false
1378
+ }
1379
+ });
1380
+ ctx.body = {
1381
+ access_token: newAccessToken,
1382
+ token_type: "Bearer",
1383
+ expires_in: expiresIn,
1384
+ refresh_token: newRefreshToken
1385
+ };
1386
+ }
984
1387
  const controllers = {
985
1388
  controller,
986
- mcp: mcpController
1389
+ mcp: mcpController,
1390
+ oauth: oauthController
987
1391
  };
988
1392
  const middlewares = {};
989
1393
  const policies = {};
@@ -1033,10 +1437,42 @@ const admin = [
1033
1437
  }
1034
1438
  }
1035
1439
  ];
1440
+ const oauth = [
1441
+ // OAuth 2.0 Authorization Server Metadata
1442
+ {
1443
+ method: "GET",
1444
+ path: "/.well-known/oauth-authorization-server",
1445
+ handler: "oauth.discovery",
1446
+ config: {
1447
+ auth: false,
1448
+ policies: []
1449
+ }
1450
+ },
1451
+ // OAuth 2.0 Authorization Endpoint
1452
+ {
1453
+ method: "GET",
1454
+ path: "/oauth/authorize",
1455
+ handler: "oauth.authorize",
1456
+ config: {
1457
+ auth: false,
1458
+ policies: []
1459
+ }
1460
+ },
1461
+ // OAuth 2.0 Token Endpoint
1462
+ {
1463
+ method: "POST",
1464
+ path: "/oauth/token",
1465
+ handler: "oauth.token",
1466
+ config: {
1467
+ auth: false,
1468
+ policies: []
1469
+ }
1470
+ }
1471
+ ];
1036
1472
  const routes = {
1037
1473
  "content-api": {
1038
1474
  type: "content-api",
1039
- routes: [...contentApi]
1475
+ routes: [...oauth, ...contentApi]
1040
1476
  },
1041
1477
  admin: {
1042
1478
  type: "admin",
@@ -1217,8 +1653,81 @@ const service = ({ strapi }) => ({
1217
1653
  return transcriptData;
1218
1654
  }
1219
1655
  });
1656
+ const PLUGIN_ID = "plugin::yt-transcript-strapi-plugin";
1657
+ const oauthService = ({ strapi }) => ({
1658
+ /**
1659
+ * Validate an OAuth access token and return the associated Strapi API token
1660
+ */
1661
+ async validateAccessToken(accessToken) {
1662
+ if (!accessToken) {
1663
+ return { valid: false, error: "No access token provided" };
1664
+ }
1665
+ try {
1666
+ const token = await strapi.documents(`${PLUGIN_ID}.oauth-token`).findFirst({
1667
+ filters: { accessToken, revoked: false }
1668
+ });
1669
+ if (!token) {
1670
+ return { valid: false, error: "Invalid access token" };
1671
+ }
1672
+ if (new Date(token.expiresAt) < /* @__PURE__ */ new Date()) {
1673
+ return { valid: false, error: "Access token expired" };
1674
+ }
1675
+ const client = await strapi.documents(`${PLUGIN_ID}.oauth-client`).findFirst({
1676
+ filters: { clientId: token.clientId, active: true }
1677
+ });
1678
+ if (!client) {
1679
+ return { valid: false, error: "OAuth client not found or inactive" };
1680
+ }
1681
+ return {
1682
+ valid: true,
1683
+ clientId: token.clientId,
1684
+ strapiApiToken: client.strapiApiToken
1685
+ };
1686
+ } catch (error) {
1687
+ strapi.log.error("[oauth] Error validating access token:", error);
1688
+ return { valid: false, error: "Token validation failed" };
1689
+ }
1690
+ },
1691
+ /**
1692
+ * Clean up expired codes and tokens (call periodically)
1693
+ */
1694
+ async cleanupExpiredTokens() {
1695
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1696
+ const expiredCodes = await strapi.documents(`${PLUGIN_ID}.oauth-code`).findMany({
1697
+ filters: {
1698
+ $or: [
1699
+ { expiresAt: { $lt: now } },
1700
+ { used: true }
1701
+ ]
1702
+ }
1703
+ });
1704
+ for (const code of expiredCodes) {
1705
+ await strapi.documents(`${PLUGIN_ID}.oauth-code`).delete({
1706
+ documentId: code.documentId
1707
+ });
1708
+ }
1709
+ const expiredTokens = await strapi.documents(`${PLUGIN_ID}.oauth-token`).findMany({
1710
+ filters: {
1711
+ $or: [
1712
+ { refreshExpiresAt: { $lt: now } },
1713
+ { revoked: true }
1714
+ ]
1715
+ }
1716
+ });
1717
+ for (const token of expiredTokens) {
1718
+ await strapi.documents(`${PLUGIN_ID}.oauth-token`).delete({
1719
+ documentId: token.documentId
1720
+ });
1721
+ }
1722
+ return {
1723
+ deletedCodes: expiredCodes.length,
1724
+ deletedTokens: expiredTokens.length
1725
+ };
1726
+ }
1727
+ });
1220
1728
  const services = {
1221
- service
1729
+ service,
1730
+ oauth: oauthService
1222
1731
  };
1223
1732
  const index = {
1224
1733
  register,