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.
- package/dist/server/index.js +521 -12
- package/dist/server/index.mjs +522 -13
- package/dist/server/src/content-types/index.d.ts +147 -0
- package/dist/server/src/content-types/oauth-client/index.d.ts +53 -0
- package/dist/server/src/content-types/oauth-code/index.d.ts +47 -0
- package/dist/server/src/content-types/oauth-token/index.d.ts +50 -0
- package/dist/server/src/controllers/index.d.ts +7 -0
- package/dist/server/src/controllers/mcp.d.ts +1 -0
- package/dist/server/src/controllers/oauth.d.ts +21 -0
- package/dist/server/src/index.d.ts +163 -0
- package/dist/server/src/routes/oauth.d.ts +10 -0
- package/dist/server/src/services/index.d.ts +9 -0
- package/dist/server/src/services/oauth.d.ts +23 -0
- package/dist/server/src/types.d.ts +33 -0
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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,
|