@automattic/mcp-wordpress-remote 0.5.2 → 0.5.3
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/Docs/authentication.md +45 -0
- package/README.md +5 -5
- package/dist/lib.d.ts +18 -12
- package/dist/lib.js +558 -407
- package/dist/proxy.js +487 -461
- package/package.json +7 -3
package/dist/proxy.js
CHANGED
|
@@ -76381,7 +76381,6 @@ import crypto3 from "crypto";
|
|
|
76381
76381
|
// src/lib/persistent-auth-config.ts
|
|
76382
76382
|
import path from "path";
|
|
76383
76383
|
import fs from "fs/promises";
|
|
76384
|
-
import fsSync from "fs";
|
|
76385
76384
|
import crypto2 from "crypto";
|
|
76386
76385
|
function getConfigDir() {
|
|
76387
76386
|
const baseConfigDir = CONFIG.WP_MCP_CONFIG_DIR;
|
|
@@ -76478,21 +76477,25 @@ async function writeTokens(serverUrlHash, tokens) {
|
|
|
76478
76477
|
...tokens,
|
|
76479
76478
|
obtained_at: Date.now()
|
|
76480
76479
|
};
|
|
76481
|
-
await
|
|
76480
|
+
await ensureConfigDir();
|
|
76481
|
+
const file2 = getConfigFilePath(serverUrlHash, "tokens.json");
|
|
76482
|
+
const temporary = `${file2}.${crypto2.randomUUID()}.tmp`;
|
|
76483
|
+
try {
|
|
76484
|
+
await fs.writeFile(temporary, JSON.stringify(tokensWithTimestamp, null, 2), {
|
|
76485
|
+
encoding: "utf-8",
|
|
76486
|
+
mode: 384,
|
|
76487
|
+
flag: "wx"
|
|
76488
|
+
});
|
|
76489
|
+
await fs.rename(temporary, file2);
|
|
76490
|
+
} finally {
|
|
76491
|
+
await fs.rm(temporary, { force: true });
|
|
76492
|
+
}
|
|
76482
76493
|
logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
76483
76494
|
} catch (error51) {
|
|
76484
76495
|
logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
|
|
76485
76496
|
throw error51;
|
|
76486
76497
|
}
|
|
76487
76498
|
}
|
|
76488
|
-
async function deleteTokens(serverUrlHash) {
|
|
76489
|
-
try {
|
|
76490
|
-
await deleteConfigFile(serverUrlHash, "tokens.json");
|
|
76491
|
-
logger.info(`Deleted tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
76492
|
-
} catch (error51) {
|
|
76493
|
-
logger.error(`Error deleting tokens for ${serverUrlHash}`, "AUTH", error51);
|
|
76494
|
-
}
|
|
76495
|
-
}
|
|
76496
76499
|
async function readClientInfo(serverUrlHash) {
|
|
76497
76500
|
try {
|
|
76498
76501
|
const clientInfo = await readJsonFile(serverUrlHash, "client_info.json");
|
|
@@ -76519,7 +76522,7 @@ function isTokenValid(tokens) {
|
|
|
76519
76522
|
if (!tokens?.access_token) {
|
|
76520
76523
|
return { isValid: false, error: "No access token" };
|
|
76521
76524
|
}
|
|
76522
|
-
if (
|
|
76525
|
+
if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
|
|
76523
76526
|
return { isValid: true };
|
|
76524
76527
|
}
|
|
76525
76528
|
const now = Date.now();
|
|
@@ -76551,30 +76554,6 @@ async function getValidTokens(serverUrlHash) {
|
|
|
76551
76554
|
}
|
|
76552
76555
|
return tokens;
|
|
76553
76556
|
}
|
|
76554
|
-
async function cleanupExpiredTokens() {
|
|
76555
|
-
try {
|
|
76556
|
-
const configDir = getConfigDir();
|
|
76557
|
-
if (!fsSync.existsSync(configDir)) {
|
|
76558
|
-
return;
|
|
76559
|
-
}
|
|
76560
|
-
const files = await fs.readdir(configDir);
|
|
76561
|
-
const tokenFiles = files.filter((file2) => file2.endsWith("_tokens.json"));
|
|
76562
|
-
let cleaned = 0;
|
|
76563
|
-
for (const file2 of tokenFiles) {
|
|
76564
|
-
const serverHash = file2.replace("_tokens.json", "");
|
|
76565
|
-
const tokens = await readTokens(serverHash);
|
|
76566
|
-
if (tokens && !isTokenValid(tokens).isValid) {
|
|
76567
|
-
await deleteTokens(serverHash);
|
|
76568
|
-
cleaned++;
|
|
76569
|
-
}
|
|
76570
|
-
}
|
|
76571
|
-
if (cleaned > 0) {
|
|
76572
|
-
logger.info(`Cleaned up ${cleaned} expired token files`, "AUTH");
|
|
76573
|
-
}
|
|
76574
|
-
} catch (error51) {
|
|
76575
|
-
logger.error("Error during token cleanup", "AUTH", error51);
|
|
76576
|
-
}
|
|
76577
|
-
}
|
|
76578
76557
|
|
|
76579
76558
|
// src/lib/port-utils.ts
|
|
76580
76559
|
var OAuthClientInfoSchema = external_exports.object({
|
|
@@ -76582,12 +76561,12 @@ var OAuthClientInfoSchema = external_exports.object({
|
|
|
76582
76561
|
client_id: external_exports.string().optional(),
|
|
76583
76562
|
client_secret: external_exports.string().optional()
|
|
76584
76563
|
});
|
|
76585
|
-
async function findAvailablePort(preferredPort) {
|
|
76564
|
+
async function findAvailablePort(preferredPort, host = "127.0.0.1") {
|
|
76586
76565
|
return new Promise((resolve, reject) => {
|
|
76587
76566
|
const server = net.createServer();
|
|
76588
76567
|
server.on("error", (err) => {
|
|
76589
76568
|
if (err.code === "EADDRINUSE") {
|
|
76590
|
-
server.listen(0);
|
|
76569
|
+
server.listen(0, host);
|
|
76591
76570
|
} else {
|
|
76592
76571
|
reject(err);
|
|
76593
76572
|
}
|
|
@@ -76598,7 +76577,7 @@ async function findAvailablePort(preferredPort) {
|
|
|
76598
76577
|
resolve(port);
|
|
76599
76578
|
});
|
|
76600
76579
|
});
|
|
76601
|
-
server.listen(preferredPort || 0);
|
|
76580
|
+
server.listen(preferredPort || 0, host);
|
|
76602
76581
|
});
|
|
76603
76582
|
}
|
|
76604
76583
|
function calculateDefaultPort(serverUrlHash) {
|
|
@@ -76626,24 +76605,17 @@ async function findExistingClientPort(serverUrlHash) {
|
|
|
76626
76605
|
function getServerUrlHash(serverUrl) {
|
|
76627
76606
|
return crypto3.createHash("md5").update(serverUrl).digest("hex");
|
|
76628
76607
|
}
|
|
76629
|
-
async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
|
|
76608
|
+
async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
|
|
76630
76609
|
const serverUrlHash = getServerUrlHash(serverUrl);
|
|
76631
76610
|
if (specifiedPort) {
|
|
76632
76611
|
return specifiedPort;
|
|
76633
76612
|
}
|
|
76634
|
-
const
|
|
76635
|
-
|
|
76636
|
-
findExistingClientPort(serverUrlHash),
|
|
76637
|
-
findAvailablePort(defaultPort)
|
|
76638
|
-
]);
|
|
76639
|
-
if (existingClientPort) {
|
|
76640
|
-
return existingClientPort;
|
|
76641
|
-
}
|
|
76642
|
-
return availablePort;
|
|
76613
|
+
const existingClientPort = await findExistingClientPort(serverUrlHash);
|
|
76614
|
+
return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
|
|
76643
76615
|
}
|
|
76644
76616
|
|
|
76645
76617
|
// src/lib/config.ts
|
|
76646
|
-
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.
|
|
76618
|
+
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.3";
|
|
76647
76619
|
function parsePositiveIntEnv(value, defaultValue) {
|
|
76648
76620
|
if (!value) {
|
|
76649
76621
|
return defaultValue;
|
|
@@ -76844,8 +76816,8 @@ async function getOAuthCallbackPort() {
|
|
|
76844
76816
|
return selectCallbackPort(
|
|
76845
76817
|
CONFIG.WP_API_URL,
|
|
76846
76818
|
CONFIG.OAUTH_CALLBACK_PORT,
|
|
76847
|
-
false
|
|
76848
|
-
|
|
76819
|
+
false,
|
|
76820
|
+
CONFIG.OAUTH_HOST
|
|
76849
76821
|
);
|
|
76850
76822
|
}
|
|
76851
76823
|
function formatConfigError(error51) {
|
|
@@ -77380,13 +77352,13 @@ var StreamableHTTPClientTransport = class {
|
|
|
77380
77352
|
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
|
|
77381
77353
|
return;
|
|
77382
77354
|
}
|
|
77383
|
-
const
|
|
77355
|
+
const delay2 = this._getNextReconnectionDelay(attemptCount);
|
|
77384
77356
|
this._reconnectionTimeout = setTimeout(() => {
|
|
77385
77357
|
this._startOrAuthSse(options).catch((error51) => {
|
|
77386
77358
|
this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error51 instanceof Error ? error51.message : String(error51)}`));
|
|
77387
77359
|
this._scheduleReconnection(options, attemptCount + 1);
|
|
77388
77360
|
});
|
|
77389
|
-
},
|
|
77361
|
+
}, delay2);
|
|
77390
77362
|
}
|
|
77391
77363
|
_handleSseStream(stream, options, isReconnectable) {
|
|
77392
77364
|
if (!stream) {
|
|
@@ -78292,10 +78264,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
78292
78264
|
var open_default = open;
|
|
78293
78265
|
|
|
78294
78266
|
// src/lib/mcp-oauth-provider.ts
|
|
78267
|
+
import lockfile from "proper-lockfile";
|
|
78268
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
78295
78269
|
import { EventEmitter } from "node:events";
|
|
78296
78270
|
|
|
78297
78271
|
// src/lib/mcp-oauth-utils.ts
|
|
78298
78272
|
import crypto4 from "crypto";
|
|
78273
|
+
|
|
78274
|
+
// src/lib/oauth-http.ts
|
|
78275
|
+
function validateOAuthUrl(value) {
|
|
78276
|
+
let url2;
|
|
78277
|
+
try {
|
|
78278
|
+
url2 = new URL(value);
|
|
78279
|
+
} catch {
|
|
78280
|
+
throw new OAuthError2("Invalid OAuth URL", "INVALID_OAUTH_URL");
|
|
78281
|
+
}
|
|
78282
|
+
const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
|
|
78283
|
+
if (url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback) || url2.username || url2.password || url2.hash) {
|
|
78284
|
+
throw new OAuthError2(
|
|
78285
|
+
"OAuth URLs require HTTPS (HTTP is allowed only on loopback)",
|
|
78286
|
+
"INSECURE_OAUTH_URL"
|
|
78287
|
+
);
|
|
78288
|
+
}
|
|
78289
|
+
return url2;
|
|
78290
|
+
}
|
|
78291
|
+
async function oauthFetch(value, init = {}) {
|
|
78292
|
+
const url2 = validateOAuthUrl(value);
|
|
78293
|
+
const headers = new Headers(
|
|
78294
|
+
url2.origin === new URL(CONFIG.WP_API_URL).origin ? getCustomHeaders() : {}
|
|
78295
|
+
);
|
|
78296
|
+
new Headers(init.headers).forEach((value2, key) => headers.set(key, value2));
|
|
78297
|
+
const response = await proxyFetch(url2.toString(), {
|
|
78298
|
+
...init,
|
|
78299
|
+
headers,
|
|
78300
|
+
redirect: "error",
|
|
78301
|
+
signal: init.signal ?? AbortSignal.timeout(CONFIG.OAUTH_TIMEOUT)
|
|
78302
|
+
});
|
|
78303
|
+
if (response.status >= 300 && response.status < 400)
|
|
78304
|
+
throw new OAuthError2("OAuth endpoint redirects are not accepted", "OAUTH_REDIRECT");
|
|
78305
|
+
return response;
|
|
78306
|
+
}
|
|
78307
|
+
|
|
78308
|
+
// src/lib/mcp-oauth-utils.ts
|
|
78299
78309
|
function generatePKCE() {
|
|
78300
78310
|
const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
|
|
78301
78311
|
const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -78323,118 +78333,99 @@ function generateCanonicalResourceURI(serverUrl) {
|
|
|
78323
78333
|
}
|
|
78324
78334
|
}
|
|
78325
78335
|
async function discoverAuthorizationServerMetadata2(authorizationServerUrl) {
|
|
78326
|
-
|
|
78327
|
-
|
|
78328
|
-
|
|
78329
|
-
const
|
|
78330
|
-
|
|
78331
|
-
|
|
78332
|
-
Accept: "application/json",
|
|
78333
|
-
...customHeaders
|
|
78334
|
-
}
|
|
78335
|
-
});
|
|
78336
|
-
if (!response.ok) {
|
|
78336
|
+
const issuer = validateOAuthUrl(authorizationServerUrl);
|
|
78337
|
+
if (issuer.search) throw new OAuthError2("Issuer must not contain a query", "INVALID_METADATA");
|
|
78338
|
+
for (const { url: url2 } of buildDiscoveryUrls(issuer)) {
|
|
78339
|
+
const response = await oauthFetch(url2.toString(), { headers: { Accept: "application/json" } });
|
|
78340
|
+
if ([404, 405].includes(response.status)) continue;
|
|
78341
|
+
if (!response.ok)
|
|
78337
78342
|
throw new OAuthError2(
|
|
78338
|
-
`
|
|
78343
|
+
`Metadata request failed (${response.status})`,
|
|
78339
78344
|
"METADATA_DISCOVERY_FAILED"
|
|
78340
78345
|
);
|
|
78341
|
-
|
|
78342
|
-
|
|
78343
|
-
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
78346
|
+
const parsed = OAuthMetadataSchema.safeParse(await response.json());
|
|
78347
|
+
if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
|
|
78344
78348
|
throw new OAuthError2(
|
|
78345
|
-
"Authorization
|
|
78349
|
+
"Authorization metadata has an invalid or mismatched issuer",
|
|
78346
78350
|
"INVALID_METADATA"
|
|
78347
78351
|
);
|
|
78348
78352
|
}
|
|
78349
|
-
|
|
78353
|
+
const metadata = parsed.data;
|
|
78354
|
+
validateOAuthUrl(metadata.issuer);
|
|
78355
|
+
validateOAuthUrl(metadata.authorization_endpoint);
|
|
78356
|
+
validateOAuthUrl(metadata.token_endpoint);
|
|
78357
|
+
if (metadata.registration_endpoint) validateOAuthUrl(metadata.registration_endpoint);
|
|
78350
78358
|
return metadata;
|
|
78351
|
-
} catch (error51) {
|
|
78352
|
-
if (error51 instanceof OAuthError2) {
|
|
78353
|
-
throw error51;
|
|
78354
|
-
}
|
|
78355
|
-
throw new OAuthError2(
|
|
78356
|
-
`Error discovering authorization server metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
78357
|
-
"METADATA_DISCOVERY_ERROR"
|
|
78358
|
-
);
|
|
78359
78359
|
}
|
|
78360
|
+
throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
|
|
78360
78361
|
}
|
|
78361
|
-
async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
78362
|
-
|
|
78363
|
-
|
|
78364
|
-
|
|
78365
|
-
|
|
78366
|
-
|
|
78367
|
-
|
|
78368
|
-
headers: {
|
|
78369
|
-
Accept: "application/json",
|
|
78370
|
-
...customHeaders
|
|
78362
|
+
async function discoverProtectedResourceMetadata(resourceUrl, resourceMetadataUrl) {
|
|
78363
|
+
const resource = validateOAuthUrl(resourceUrl);
|
|
78364
|
+
const candidates = resourceMetadataUrl ? [{ url: resourceMetadataUrl, expected: resourceUrl }] : [
|
|
78365
|
+
...resource.pathname !== "/" ? [
|
|
78366
|
+
{
|
|
78367
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`,
|
|
78368
|
+
expected: resourceUrl
|
|
78371
78369
|
}
|
|
78372
|
-
|
|
78373
|
-
|
|
78370
|
+
] : [],
|
|
78371
|
+
{
|
|
78372
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource`,
|
|
78373
|
+
expected: resource.pathname === "/" ? resourceUrl : resource.origin
|
|
78374
|
+
}
|
|
78375
|
+
];
|
|
78376
|
+
for (const candidate of candidates) {
|
|
78377
|
+
const response = await oauthFetch(candidate.url, { headers: { Accept: "application/json" } });
|
|
78378
|
+
if (!resourceMetadataUrl && [404, 405].includes(response.status)) continue;
|
|
78379
|
+
if (!response.ok)
|
|
78374
78380
|
throw new OAuthError2(
|
|
78375
|
-
`
|
|
78381
|
+
`Resource metadata request failed (${response.status})`,
|
|
78376
78382
|
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
78377
78383
|
);
|
|
78378
|
-
|
|
78379
|
-
|
|
78380
|
-
if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
|
|
78384
|
+
const parsed = OAuthProtectedResourceMetadataSchema.safeParse(await response.json());
|
|
78385
|
+
if (!parsed.success || parsed.data.resource !== candidate.expected || !parsed.data.authorization_servers?.length) {
|
|
78381
78386
|
throw new OAuthError2(
|
|
78382
|
-
"Protected resource metadata
|
|
78387
|
+
"Protected resource metadata has an invalid or mismatched resource",
|
|
78383
78388
|
"INVALID_RESOURCE_METADATA"
|
|
78384
78389
|
);
|
|
78385
78390
|
}
|
|
78386
|
-
|
|
78387
|
-
return
|
|
78388
|
-
} catch (error51) {
|
|
78389
|
-
if (error51 instanceof OAuthError2) {
|
|
78390
|
-
throw error51;
|
|
78391
|
-
}
|
|
78392
|
-
throw new OAuthError2(
|
|
78393
|
-
`Error discovering protected resource metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
78394
|
-
"RESOURCE_METADATA_DISCOVERY_ERROR"
|
|
78395
|
-
);
|
|
78391
|
+
for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
|
|
78392
|
+
return parsed.data;
|
|
78396
78393
|
}
|
|
78394
|
+
throw new OAuthError2(
|
|
78395
|
+
"No protected resource metadata found",
|
|
78396
|
+
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
78397
|
+
);
|
|
78397
78398
|
}
|
|
78398
78399
|
function parseWWWAuthenticateHeader(headerValue2) {
|
|
78399
|
-
const
|
|
78400
|
-
const
|
|
78401
|
-
|
|
78402
|
-
const
|
|
78403
|
-
const
|
|
78404
|
-
|
|
78405
|
-
|
|
78406
|
-
|
|
78407
|
-
|
|
78408
|
-
|
|
78409
|
-
|
|
78410
|
-
|
|
78411
|
-
|
|
78412
|
-
|
|
78413
|
-
|
|
78414
|
-
|
|
78415
|
-
|
|
78416
|
-
|
|
78417
|
-
|
|
78418
|
-
break;
|
|
78419
|
-
case "error_uri":
|
|
78420
|
-
result.error_uri = value;
|
|
78421
|
-
break;
|
|
78422
|
-
case "resource_metadata_url":
|
|
78423
|
-
result.resource_metadata_url = value;
|
|
78424
|
-
break;
|
|
78425
|
-
}
|
|
78400
|
+
const bearer = /(?:^|,)\s*Bearer(?:\s+|$)/i.exec(headerValue2);
|
|
78401
|
+
const result = { scheme: bearer ? "Bearer" : headerValue2.split(/\s+/)[0] };
|
|
78402
|
+
if (!bearer) return result;
|
|
78403
|
+
const parameters = headerValue2.slice(bearer.index + bearer[0].length).split(/,\s*(?=[A-Za-z][\w-]*\s+(?![=]))/)[0];
|
|
78404
|
+
const known = /* @__PURE__ */ new Set([
|
|
78405
|
+
"realm",
|
|
78406
|
+
"scope",
|
|
78407
|
+
"error",
|
|
78408
|
+
"error_description",
|
|
78409
|
+
"error_uri",
|
|
78410
|
+
"resource_metadata"
|
|
78411
|
+
]);
|
|
78412
|
+
for (const match of parameters.matchAll(/([\w-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^,\s]+))/g)) {
|
|
78413
|
+
const key = match[1].toLowerCase();
|
|
78414
|
+
if (known.has(key))
|
|
78415
|
+
result[key] = (match[2] ?? match[3]).replace(
|
|
78416
|
+
/\\(.)/g,
|
|
78417
|
+
"$1"
|
|
78418
|
+
);
|
|
78426
78419
|
}
|
|
78427
78420
|
return result;
|
|
78428
78421
|
}
|
|
78429
78422
|
async function registerDynamicClient(registrationEndpoint, registrationRequest) {
|
|
78430
78423
|
try {
|
|
78431
78424
|
logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
|
|
78432
|
-
const
|
|
78433
|
-
const response = await proxyFetch(registrationEndpoint, {
|
|
78425
|
+
const response = await oauthFetch(registrationEndpoint, {
|
|
78434
78426
|
method: "POST",
|
|
78435
78427
|
headers: {
|
|
78436
|
-
"Content-Type": "application/json"
|
|
78437
|
-
...customHeaders
|
|
78428
|
+
"Content-Type": "application/json"
|
|
78438
78429
|
},
|
|
78439
78430
|
body: JSON.stringify(registrationRequest)
|
|
78440
78431
|
});
|
|
@@ -78446,12 +78437,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
|
|
|
78446
78437
|
);
|
|
78447
78438
|
}
|
|
78448
78439
|
const registrationResponse = await response.json();
|
|
78449
|
-
if (!registrationResponse.client_id) {
|
|
78440
|
+
if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
|
|
78450
78441
|
throw new OAuthError2(
|
|
78451
78442
|
"Dynamic client registration response missing client_id",
|
|
78452
78443
|
"INVALID_REGISTRATION_RESPONSE"
|
|
78453
78444
|
);
|
|
78454
78445
|
}
|
|
78446
|
+
if (registrationResponse.token_endpoint_auth_method && registrationResponse.token_endpoint_auth_method !== registrationRequest.token_endpoint_auth_method) {
|
|
78447
|
+
throw new OAuthError2(
|
|
78448
|
+
"Authorization server changed the requested client authentication method",
|
|
78449
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
78450
|
+
);
|
|
78451
|
+
}
|
|
78452
|
+
if (registrationResponse.redirect_uris && (!Array.isArray(registrationResponse.redirect_uris) || !registrationRequest.redirect_uris.every(
|
|
78453
|
+
(uri) => registrationResponse.redirect_uris.includes(uri)
|
|
78454
|
+
))) {
|
|
78455
|
+
throw new OAuthError2(
|
|
78456
|
+
"Authorization server changed the registered callback URI",
|
|
78457
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
78458
|
+
);
|
|
78459
|
+
}
|
|
78455
78460
|
logger.oauth("Dynamic client registration successful");
|
|
78456
78461
|
logger.debug("Client ID obtained", "OAUTH", {
|
|
78457
78462
|
client_id: registrationResponse.client_id,
|
|
@@ -78481,12 +78486,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
78481
78486
|
if (resource) {
|
|
78482
78487
|
params.set("resource", resource);
|
|
78483
78488
|
}
|
|
78484
|
-
const
|
|
78485
|
-
const response = await proxyFetch(tokenEndpoint, {
|
|
78489
|
+
const response = await oauthFetch(tokenEndpoint, {
|
|
78486
78490
|
method: "POST",
|
|
78487
78491
|
headers: {
|
|
78488
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
78489
|
-
...customHeaders
|
|
78492
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
78490
78493
|
},
|
|
78491
78494
|
body: params.toString()
|
|
78492
78495
|
});
|
|
@@ -78497,7 +78500,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
78497
78500
|
"TOKEN_EXCHANGE_FAILED"
|
|
78498
78501
|
);
|
|
78499
78502
|
}
|
|
78500
|
-
const tokenResponse = await response.json();
|
|
78503
|
+
const tokenResponse = OAuthTokensSchema.parse(await response.json());
|
|
78504
|
+
if (tokenResponse.token_type.toLowerCase() !== "bearer")
|
|
78505
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
78501
78506
|
if (!tokenResponse.access_token) {
|
|
78502
78507
|
throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
|
|
78503
78508
|
}
|
|
@@ -78526,15 +78531,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
|
|
|
78526
78531
|
// OAuth 2.1 uses authorization code flow
|
|
78527
78532
|
client_id: clientId,
|
|
78528
78533
|
redirect_uri: redirectUri,
|
|
78529
|
-
scope: scopes.join(" "),
|
|
78530
78534
|
state,
|
|
78531
78535
|
code_challenge: codeChallenge,
|
|
78532
78536
|
code_challenge_method: "S256"
|
|
78533
78537
|
});
|
|
78538
|
+
if (scopes.length) params.set("scope", scopes.join(" "));
|
|
78534
78539
|
if (resource) {
|
|
78535
78540
|
params.set("resource", resource);
|
|
78536
78541
|
}
|
|
78537
|
-
|
|
78542
|
+
validateOAuthUrl(authorizationEndpoint);
|
|
78543
|
+
const url2 = new URL(authorizationEndpoint);
|
|
78544
|
+
params.forEach((value, key) => url2.searchParams.set(key, value));
|
|
78545
|
+
return url2.toString();
|
|
78538
78546
|
}
|
|
78539
78547
|
function generateSecureState() {
|
|
78540
78548
|
return crypto4.randomBytes(32).toString("base64url");
|
|
@@ -78615,110 +78623,26 @@ var AUTHORIZATION_CODE_HTML = `
|
|
|
78615
78623
|
</div>
|
|
78616
78624
|
|
|
78617
78625
|
<script>
|
|
78618
|
-
|
|
78619
|
-
|
|
78620
|
-
|
|
78621
|
-
|
|
78622
|
-
|
|
78623
|
-
|
|
78624
|
-
|
|
78625
|
-
|
|
78626
|
-
|
|
78627
|
-
|
|
78628
|
-
|
|
78629
|
-
|
|
78630
|
-
|
|
78631
|
-
|
|
78632
|
-
|
|
78633
|
-
|
|
78634
|
-
|
|
78635
|
-
|
|
78636
|
-
|
|
78637
|
-
|
|
78638
|
-
// Check for authorization code first (OAuth 2.1 flow)
|
|
78639
|
-
if (urlParams.has('code')) {
|
|
78640
|
-
const authData = {
|
|
78641
|
-
code: urlParams.get('code'),
|
|
78642
|
-
state: urlParams.get('state'),
|
|
78643
|
-
// Additional parameters for validation
|
|
78644
|
-
iss: urlParams.get('iss'),
|
|
78645
|
-
};
|
|
78646
|
-
|
|
78647
|
-
// Send authorization code to server for token exchange
|
|
78648
|
-
fetch('/oauth/callback', {
|
|
78649
|
-
method: 'POST',
|
|
78650
|
-
headers: {
|
|
78651
|
-
'Content-Type': 'application/json'
|
|
78652
|
-
},
|
|
78653
|
-
body: JSON.stringify(authData)
|
|
78654
|
-
})
|
|
78655
|
-
.then(response => {
|
|
78656
|
-
if (response.ok) {
|
|
78657
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
78658
|
-
displayDetails('You can safely close this window.');
|
|
78659
|
-
} else {
|
|
78660
|
-
return response.json().then(err => {
|
|
78661
|
-
throw new Error(err.error || 'Failed to exchange authorization code');
|
|
78662
|
-
});
|
|
78663
|
-
}
|
|
78664
|
-
})
|
|
78665
|
-
.catch(error => {
|
|
78666
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
78667
|
-
displayDetails(error.message + '. Please try again.');
|
|
78668
|
-
});
|
|
78669
|
-
}
|
|
78670
|
-
// Check for access token in URL fragment (implicit flow)
|
|
78671
|
-
else if (hashParams.has('access_token')) {
|
|
78672
|
-
const tokens = {
|
|
78673
|
-
access_token: hashParams.get('access_token'),
|
|
78674
|
-
token_type: hashParams.get('token_type') || 'Bearer',
|
|
78675
|
-
expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600,
|
|
78676
|
-
scope: hashParams.get('scope'),
|
|
78677
|
-
state: hashParams.get('state'),
|
|
78678
|
-
obtained_at: Date.now()
|
|
78679
|
-
};
|
|
78680
|
-
|
|
78681
|
-
// Send tokens to server for storage
|
|
78682
|
-
fetch('/oauth/tokens', {
|
|
78683
|
-
method: 'POST',
|
|
78684
|
-
headers: {
|
|
78685
|
-
'Content-Type': 'application/json'
|
|
78686
|
-
},
|
|
78687
|
-
body: JSON.stringify(tokens)
|
|
78688
|
-
})
|
|
78689
|
-
.then(response => {
|
|
78690
|
-
if (response.ok) {
|
|
78691
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
78692
|
-
displayDetails('You can safely close this window.');
|
|
78693
|
-
} else {
|
|
78694
|
-
return response.json().then(err => {
|
|
78695
|
-
throw new Error(err.error || 'Failed to store access token');
|
|
78696
|
-
});
|
|
78697
|
-
}
|
|
78698
|
-
})
|
|
78699
|
-
.catch(error => {
|
|
78700
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
78701
|
-
displayDetails(error.message + '. Please try again.');
|
|
78702
|
-
});
|
|
78703
|
-
}
|
|
78704
|
-
// Check for errors in query parameters
|
|
78705
|
-
else if (urlParams.has('error')) {
|
|
78706
|
-
const error = urlParams.get('error');
|
|
78707
|
-
const errorDescription = urlParams.get('error_description');
|
|
78708
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
78709
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
78710
|
-
}
|
|
78711
|
-
// Check for errors in URL fragment (implicit flow errors)
|
|
78712
|
-
else if (hashParams.has('error')) {
|
|
78713
|
-
const error = hashParams.get('error');
|
|
78714
|
-
const errorDescription = hashParams.get('error_description');
|
|
78715
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
78716
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
78717
|
-
}
|
|
78718
|
-
// No authorization code or access token found
|
|
78719
|
-
else {
|
|
78720
|
-
displayStatus('\u274C No authorization code or access token received', 'error');
|
|
78721
|
-
displayDetails('Please try the authorization process again.');
|
|
78626
|
+
const query = new URLSearchParams(window.location.search);
|
|
78627
|
+
const fragment = new URLSearchParams(window.location.hash.substring(1));
|
|
78628
|
+
const status = document.getElementById('status');
|
|
78629
|
+
const details = document.getElementById('details');
|
|
78630
|
+
const data = Object.fromEntries(query.has('code') || query.has('error') ? query : fragment);
|
|
78631
|
+
const endpoint = data.access_token ? '/oauth/tokens' : '/oauth/callback';
|
|
78632
|
+
if (data.code || data.error || data.access_token) {
|
|
78633
|
+
fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
|
|
78634
|
+
.then(async response => {
|
|
78635
|
+
const result = await response.json();
|
|
78636
|
+
if (!response.ok) throw new Error(result.error || 'Authorization response was rejected');
|
|
78637
|
+
status.textContent = data.access_token ? 'Authorization successful' : 'Authorization code received';
|
|
78638
|
+
status.className = 'success';
|
|
78639
|
+
details.textContent = data.access_token ? 'You can close this window.' : 'Return to your app to check the connection. Token exchange may still fail.';
|
|
78640
|
+
})
|
|
78641
|
+
.catch(error => { status.textContent = 'Authorization failed'; status.className = 'error'; details.textContent = error.message; })
|
|
78642
|
+
.finally(() => { document.getElementById('spinner').style.display = 'none'; });
|
|
78643
|
+
} else {
|
|
78644
|
+
status.textContent = 'No authorization response received';
|
|
78645
|
+
document.getElementById('spinner').style.display = 'none';
|
|
78722
78646
|
}
|
|
78723
78647
|
</script>
|
|
78724
78648
|
</body>
|
|
@@ -78733,6 +78657,17 @@ var OAuthCallbackServer = class {
|
|
|
78733
78657
|
this.setupRoutes();
|
|
78734
78658
|
}
|
|
78735
78659
|
setupRoutes() {
|
|
78660
|
+
this.app.use((req, res, next) => {
|
|
78661
|
+
const host = this.options.host === "::1" ? "[::1]" : this.options.host;
|
|
78662
|
+
const origin = req.headers.origin;
|
|
78663
|
+
const port = this.server?.address();
|
|
78664
|
+
const expectedOrigin = `http://${host}:${port && typeof port !== "string" ? port.port : this.options.port}`;
|
|
78665
|
+
if (req.hostname !== host || origin && origin !== expectedOrigin) {
|
|
78666
|
+
res.status(403).json({ error: "Untrusted callback origin" });
|
|
78667
|
+
return;
|
|
78668
|
+
}
|
|
78669
|
+
next();
|
|
78670
|
+
});
|
|
78736
78671
|
this.app.use(import_express.default.json());
|
|
78737
78672
|
this.app.get("/oauth/callback", (req, res) => {
|
|
78738
78673
|
logger.oauth("OAuth 2.1 authorization callback page requested");
|
|
@@ -78740,46 +78675,47 @@ var OAuthCallbackServer = class {
|
|
|
78740
78675
|
});
|
|
78741
78676
|
this.app.post("/oauth/callback", async (req, res) => {
|
|
78742
78677
|
try {
|
|
78743
|
-
const { code, state, iss } = req.body;
|
|
78744
|
-
|
|
78745
|
-
|
|
78746
|
-
|
|
78747
|
-
|
|
78748
|
-
|
|
78749
|
-
|
|
78750
|
-
state: state || "none",
|
|
78751
|
-
issuer: iss || "none"
|
|
78752
|
-
});
|
|
78678
|
+
const { code, state, iss, error: error51 } = req.body;
|
|
78679
|
+
this.validateResponse(state, iss);
|
|
78680
|
+
if (error51) throw new OAuthError2("Authorization denied or failed", "AUTH_DENIED");
|
|
78681
|
+
if (this.options.flow === "implicit")
|
|
78682
|
+
throw new OAuthError2("Authorization-code callback is disabled", "WRONG_FLOW");
|
|
78683
|
+
if (typeof code !== "string" || !code)
|
|
78684
|
+
throw new OAuthError2("No authorization code received", "INVALID_CODE");
|
|
78753
78685
|
this.events.emit("oauth-code-received", { code, state, iss });
|
|
78754
|
-
res.json({
|
|
78686
|
+
res.json({
|
|
78687
|
+
success: true,
|
|
78688
|
+
message: "Authorization code received; check connection status in the app"
|
|
78689
|
+
});
|
|
78755
78690
|
} catch (error51) {
|
|
78756
|
-
|
|
78757
|
-
|
|
78758
|
-
|
|
78759
|
-
res.status(400).json({ error: errorMessage });
|
|
78691
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid authorization response");
|
|
78692
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
78693
|
+
res.status(400).json({ error: failure.message });
|
|
78760
78694
|
}
|
|
78761
78695
|
});
|
|
78762
78696
|
this.app.post("/oauth/tokens", async (req, res) => {
|
|
78697
|
+
if (this.options.flow !== "implicit") {
|
|
78698
|
+
res.status(404).json({ error: "Implicit token callback is disabled" });
|
|
78699
|
+
return;
|
|
78700
|
+
}
|
|
78763
78701
|
try {
|
|
78764
|
-
|
|
78765
|
-
if (!
|
|
78766
|
-
throw new OAuthError2("
|
|
78767
|
-
|
|
78768
|
-
|
|
78769
|
-
|
|
78770
|
-
|
|
78771
|
-
|
|
78772
|
-
|
|
78773
|
-
|
|
78774
|
-
});
|
|
78702
|
+
this.validateResponse(req.body.state, req.body.iss);
|
|
78703
|
+
if (typeof req.body.access_token !== "string" || !req.body.access_token || req.body.token_type && req.body.token_type.toLowerCase() !== "bearer")
|
|
78704
|
+
throw new OAuthError2("Invalid implicit token response");
|
|
78705
|
+
const tokens = {
|
|
78706
|
+
access_token: req.body.access_token,
|
|
78707
|
+
token_type: "Bearer",
|
|
78708
|
+
expires_in: req.body.expires_in === void 0 ? void 0 : Number(req.body.expires_in),
|
|
78709
|
+
scope: req.body.scope,
|
|
78710
|
+
obtained_at: Date.now()
|
|
78711
|
+
};
|
|
78775
78712
|
await writeTokens(this.options.serverUrlHash, tokens);
|
|
78776
78713
|
this.events.emit("oauth-success", tokens);
|
|
78777
|
-
res.json({ success: true
|
|
78714
|
+
res.json({ success: true });
|
|
78778
78715
|
} catch (error51) {
|
|
78779
|
-
|
|
78780
|
-
|
|
78781
|
-
|
|
78782
|
-
res.status(400).json({ error: errorMessage });
|
|
78716
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid implicit response");
|
|
78717
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
78718
|
+
res.status(400).json({ error: failure.message });
|
|
78783
78719
|
}
|
|
78784
78720
|
});
|
|
78785
78721
|
this.app.get("/oauth/health", (req, res) => {
|
|
@@ -78796,10 +78732,17 @@ var OAuthCallbackServer = class {
|
|
|
78796
78732
|
}
|
|
78797
78733
|
);
|
|
78798
78734
|
}
|
|
78735
|
+
validateResponse(state, issuer) {
|
|
78736
|
+
if (!this.options.state || state !== this.options.state)
|
|
78737
|
+
throw new OAuthError2("Invalid callback state", "INVALID_STATE");
|
|
78738
|
+
if ((issuer !== void 0 || this.options.requireIssuer) && issuer !== this.options.issuer)
|
|
78739
|
+
throw new OAuthError2("Invalid callback issuer", "INVALID_ISSUER");
|
|
78740
|
+
}
|
|
78799
78741
|
async start() {
|
|
78800
78742
|
return new Promise((resolve, reject) => {
|
|
78801
78743
|
try {
|
|
78802
78744
|
this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
|
|
78745
|
+
clearTimeout(this.startupTimer);
|
|
78803
78746
|
if (error51) {
|
|
78804
78747
|
reject(error51);
|
|
78805
78748
|
return;
|
|
@@ -78810,11 +78753,12 @@ var OAuthCallbackServer = class {
|
|
|
78810
78753
|
resolve();
|
|
78811
78754
|
});
|
|
78812
78755
|
this.server.on("error", (error51) => {
|
|
78756
|
+
clearTimeout(this.startupTimer);
|
|
78813
78757
|
logger.error("OAuth callback server error", "OAUTH", error51);
|
|
78814
78758
|
reject(error51);
|
|
78815
78759
|
});
|
|
78816
78760
|
if (this.options.timeout) {
|
|
78817
|
-
setTimeout(() => {
|
|
78761
|
+
this.startupTimer = setTimeout(() => {
|
|
78818
78762
|
if (!this.server?.listening) {
|
|
78819
78763
|
reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
|
|
78820
78764
|
}
|
|
@@ -78826,6 +78770,7 @@ var OAuthCallbackServer = class {
|
|
|
78826
78770
|
});
|
|
78827
78771
|
}
|
|
78828
78772
|
async stop() {
|
|
78773
|
+
clearTimeout(this.startupTimer);
|
|
78829
78774
|
if (!this.server) return;
|
|
78830
78775
|
const server = this.server;
|
|
78831
78776
|
this.server = null;
|
|
@@ -78857,7 +78802,9 @@ var MCPOAuthProvider = class {
|
|
|
78857
78802
|
this.authServerMetadata = null;
|
|
78858
78803
|
this.resourceMetadata = null;
|
|
78859
78804
|
const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
|
|
78860
|
-
const defaultScopes = getDefaultOAuthScopes();
|
|
78805
|
+
const defaultScopes = CONFIG.OAUTH_SCOPES ? getDefaultOAuthScopes() : [];
|
|
78806
|
+
this.explicitScopes = options.scopes ?? defaultScopes;
|
|
78807
|
+
this.configuredClientId = options.clientId || CONFIG.WP_OAUTH_CLIENT_ID || void 0;
|
|
78861
78808
|
this.config = {
|
|
78862
78809
|
serverUrl,
|
|
78863
78810
|
resource: generateCanonicalResourceURI(serverUrl),
|
|
@@ -78916,187 +78863,217 @@ var MCPOAuthProvider = class {
|
|
|
78916
78863
|
/**
|
|
78917
78864
|
* Discover OAuth 2.1 server endpoints using MCP-compliant discovery
|
|
78918
78865
|
*/
|
|
78919
|
-
async discoverOAuthEndpoints() {
|
|
78920
|
-
|
|
78921
|
-
|
|
78922
|
-
|
|
78923
|
-
|
|
78924
|
-
|
|
78925
|
-
|
|
78926
|
-
|
|
78927
|
-
|
|
78928
|
-
|
|
78929
|
-
|
|
78930
|
-
|
|
78931
|
-
|
|
78932
|
-
|
|
78933
|
-
|
|
78934
|
-
|
|
78935
|
-
|
|
78936
|
-
|
|
78937
|
-
|
|
78938
|
-
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
78939
|
-
} else {
|
|
78940
|
-
throw new OAuthError2("Could not determine authorization server URL", "DISCOVERY_FAILED");
|
|
78941
|
-
}
|
|
78942
|
-
} catch (error51) {
|
|
78943
|
-
logger.error("OAuth endpoint discovery failed", "OAUTH", error51);
|
|
78944
|
-
throw error51;
|
|
78945
|
-
}
|
|
78946
|
-
}
|
|
78947
|
-
/**
|
|
78948
|
-
* Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
|
|
78949
|
-
*/
|
|
78950
|
-
async discoverVia401Response() {
|
|
78951
|
-
try {
|
|
78952
|
-
logger.oauth("Attempting discovery via 401 response method");
|
|
78953
|
-
const customHeaders = getCustomHeaders();
|
|
78954
|
-
const response = await proxyFetch(this.config.serverUrl, {
|
|
78955
|
-
headers: {
|
|
78956
|
-
Accept: "application/json",
|
|
78957
|
-
...customHeaders
|
|
78958
|
-
}
|
|
78959
|
-
});
|
|
78960
|
-
if (response.status === 401) {
|
|
78961
|
-
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
78962
|
-
if (wwwAuthHeader) {
|
|
78963
|
-
const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
|
|
78964
|
-
logger.oauth("WWW-Authenticate header parsed successfully");
|
|
78965
|
-
if (authInfo.resource_metadata_url) {
|
|
78966
|
-
const metadataResponse = await proxyFetch(authInfo.resource_metadata_url, {
|
|
78967
|
-
headers: {
|
|
78968
|
-
Accept: "application/json",
|
|
78969
|
-
...customHeaders
|
|
78970
|
-
}
|
|
78971
|
-
});
|
|
78972
|
-
if (metadataResponse.ok) {
|
|
78973
|
-
this.resourceMetadata = await metadataResponse.json();
|
|
78974
|
-
logger.oauth("Protected resource metadata obtained via 401 response");
|
|
78975
|
-
}
|
|
78976
|
-
}
|
|
78977
|
-
}
|
|
78978
|
-
}
|
|
78979
|
-
} catch (error51) {
|
|
78980
|
-
logger.warn("401 response discovery method failed", "OAUTH", error51);
|
|
78981
|
-
}
|
|
78866
|
+
async discoverOAuthEndpoints(challenge) {
|
|
78867
|
+
this.resourceMetadata = await discoverProtectedResourceMetadata(
|
|
78868
|
+
this.config.serverUrl,
|
|
78869
|
+
challenge?.resource_metadata
|
|
78870
|
+
);
|
|
78871
|
+
this.authServerMetadata = await discoverAuthorizationServerMetadata2(
|
|
78872
|
+
this.resourceMetadata.authorization_servers[0]
|
|
78873
|
+
);
|
|
78874
|
+
this.config.resource = this.resourceMetadata.resource;
|
|
78875
|
+
this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
|
|
78876
|
+
this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
|
|
78877
|
+
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
78878
|
+
this.config.clientId = this.configuredClientId;
|
|
78879
|
+
const challenged = challenge?.scope?.split(/\s+/).filter(Boolean);
|
|
78880
|
+
this.config.scopes = [
|
|
78881
|
+
...new Set(
|
|
78882
|
+
challenged ?? (this.explicitScopes.length ? this.explicitScopes : this.resourceMetadata.scopes_supported ?? [])
|
|
78883
|
+
)
|
|
78884
|
+
];
|
|
78982
78885
|
}
|
|
78983
|
-
/**
|
|
78984
|
-
* Get authorization server URL from discovered metadata
|
|
78985
|
-
*/
|
|
78986
78886
|
getAuthorizationServerUrl() {
|
|
78987
|
-
|
|
78988
|
-
return this.resourceMetadata.authorization_servers[0];
|
|
78989
|
-
}
|
|
78990
|
-
try {
|
|
78991
|
-
const url2 = new URL(this.config.serverUrl);
|
|
78992
|
-
return url2.origin;
|
|
78993
|
-
} catch {
|
|
78994
|
-
return null;
|
|
78995
|
-
}
|
|
78887
|
+
return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
|
|
78996
78888
|
}
|
|
78997
78889
|
/**
|
|
78998
78890
|
* Perform dynamic client registration if needed and supported
|
|
78999
78891
|
*/
|
|
79000
78892
|
async ensureClientRegistration() {
|
|
78893
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
78894
|
+
const stored = await readClientInfo(this.serverUrlHash);
|
|
79001
78895
|
if (this.config.clientId) {
|
|
79002
|
-
|
|
78896
|
+
if (stored?.issuer && stored.issuer !== issuer && stored.client_id === this.config.clientId) {
|
|
78897
|
+
throw new OAuthError2(
|
|
78898
|
+
"Configured client belongs to another authorization server; update its registration",
|
|
78899
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
78900
|
+
);
|
|
78901
|
+
}
|
|
78902
|
+
await writeClientInfo(this.serverUrlHash, { client_id: this.config.clientId, issuer });
|
|
79003
78903
|
return;
|
|
79004
78904
|
}
|
|
79005
|
-
|
|
79006
|
-
|
|
79007
|
-
this.config.clientId = storedClientInfo.client_id;
|
|
79008
|
-
logger.oauth("Reusing persisted client registration, skipping dynamic registration");
|
|
78905
|
+
if (stored?.issuer === issuer && typeof stored.client_id === "string" && stored.client_id.length > 0) {
|
|
78906
|
+
this.config.clientId = stored.client_id;
|
|
79009
78907
|
return;
|
|
79010
78908
|
}
|
|
79011
|
-
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
78909
|
+
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
79012
78910
|
throw new OAuthError2(
|
|
79013
|
-
"No client
|
|
78911
|
+
"No issuer-bound client registration; configure a client ID or enable registration",
|
|
79014
78912
|
"NO_CLIENT_ID"
|
|
79015
78913
|
);
|
|
79016
|
-
|
|
79017
|
-
if (!this.authServerMetadata?.registration_endpoint) {
|
|
78914
|
+
if (!this.authServerMetadata?.registration_endpoint)
|
|
79018
78915
|
throw new OAuthError2(
|
|
79019
|
-
"
|
|
78916
|
+
"Authorization server does not support dynamic registration; configure its client ID",
|
|
79020
78917
|
"NO_DYNAMIC_REGISTRATION"
|
|
79021
78918
|
);
|
|
79022
|
-
|
|
79023
|
-
|
|
79024
|
-
|
|
79025
|
-
|
|
79026
|
-
|
|
79027
|
-
|
|
79028
|
-
|
|
79029
|
-
|
|
79030
|
-
|
|
79031
|
-
|
|
79032
|
-
|
|
79033
|
-
|
|
79034
|
-
|
|
79035
|
-
|
|
79036
|
-
|
|
79037
|
-
|
|
79038
|
-
|
|
79039
|
-
|
|
79040
|
-
|
|
79041
|
-
};
|
|
79042
|
-
await writeClientInfo(this.serverUrlHash, clientInfo);
|
|
79043
|
-
this.config.clientId = registrationResponse.client_id;
|
|
79044
|
-
logger.oauth("Dynamic client registration completed successfully");
|
|
79045
|
-
} catch (error51) {
|
|
79046
|
-
logger.error("Dynamic client registration failed", "OAUTH", error51);
|
|
79047
|
-
throw error51;
|
|
79048
|
-
}
|
|
78919
|
+
const request = {
|
|
78920
|
+
redirect_uris: [this.config.redirectUri],
|
|
78921
|
+
application_type: "native",
|
|
78922
|
+
token_endpoint_auth_method: "none",
|
|
78923
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
78924
|
+
response_types: ["code"],
|
|
78925
|
+
client_name: "WordPress MCP Remote",
|
|
78926
|
+
...this.config.scopes.length ? { scope: this.config.scopes.join(" ") } : {}
|
|
78927
|
+
};
|
|
78928
|
+
const registered = await registerDynamicClient(
|
|
78929
|
+
this.authServerMetadata.registration_endpoint,
|
|
78930
|
+
request
|
|
78931
|
+
);
|
|
78932
|
+
await writeClientInfo(this.serverUrlHash, {
|
|
78933
|
+
...request,
|
|
78934
|
+
client_id: registered.client_id,
|
|
78935
|
+
issuer
|
|
78936
|
+
});
|
|
78937
|
+
this.config.clientId = registered.client_id;
|
|
79049
78938
|
}
|
|
79050
78939
|
/**
|
|
79051
78940
|
* Initiate MCP-compliant OAuth 2.1 authorization flow
|
|
79052
78941
|
*/
|
|
79053
|
-
|
|
79054
|
-
|
|
79055
|
-
|
|
79056
|
-
|
|
79057
|
-
|
|
79058
|
-
|
|
79059
|
-
const
|
|
79060
|
-
|
|
79061
|
-
|
|
79062
|
-
|
|
78942
|
+
satisfies(tokens, request) {
|
|
78943
|
+
return !!tokens && tokens.access_token !== request.rejectedToken && (!request.scopes?.length || request.scopes.every((scope) => tokens.scope?.split(" ").includes(scope)));
|
|
78944
|
+
}
|
|
78945
|
+
async authorize(request = {}) {
|
|
78946
|
+
if (this.authPromise) await this.authPromise;
|
|
78947
|
+
if (!this.authPromise) this.authPromise = this.authorizeWithLock(request);
|
|
78948
|
+
const pending = this.authPromise;
|
|
78949
|
+
try {
|
|
78950
|
+
await pending;
|
|
78951
|
+
} finally {
|
|
78952
|
+
if (this.authPromise === pending) this.authPromise = null;
|
|
79063
78953
|
}
|
|
79064
|
-
|
|
79065
|
-
|
|
78954
|
+
}
|
|
78955
|
+
async authorizeWithLock(request) {
|
|
78956
|
+
const valid = await this.tokens();
|
|
78957
|
+
if (this.satisfies(valid, request)) return valid;
|
|
78958
|
+
await ensureConfigDir();
|
|
78959
|
+
const file2 = getConfigFilePath(this.serverUrlHash, "oauth");
|
|
78960
|
+
let release;
|
|
79066
78961
|
try {
|
|
79067
|
-
await
|
|
79068
|
-
logger.oauth("OAuth 2.1 authorization completed successfully");
|
|
78962
|
+
release = await lockfile.lock(file2, { realpath: false });
|
|
79069
78963
|
} catch (error51) {
|
|
79070
|
-
|
|
79071
|
-
|
|
78964
|
+
if (error51.code !== "ELOCKED") throw error51;
|
|
78965
|
+
const deadline = Date.now() + this.config.timeout;
|
|
78966
|
+
while (Date.now() < deadline) {
|
|
78967
|
+
const tokens = await this.tokens();
|
|
78968
|
+
if (this.satisfies(tokens, request)) return tokens;
|
|
78969
|
+
if (!await lockfile.check(file2, { realpath: false })) {
|
|
78970
|
+
const completed = await this.tokens();
|
|
78971
|
+
if (this.satisfies(completed, request)) return completed;
|
|
78972
|
+
throw new OAuthError2("Authentication in another process did not complete", "AUTH_FAILED");
|
|
78973
|
+
}
|
|
78974
|
+
await delay(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
78975
|
+
}
|
|
78976
|
+
throw new OAuthError2("Timeout waiting for authentication in another process", "LOCK_TIMEOUT");
|
|
78977
|
+
}
|
|
78978
|
+
try {
|
|
78979
|
+
const completed = await this.tokens();
|
|
78980
|
+
if (this.satisfies(completed, request)) return completed;
|
|
78981
|
+
await this.discoverOAuthEndpoints(request.challenge);
|
|
78982
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
78983
|
+
const stored = await readTokens(this.serverUrlHash);
|
|
78984
|
+
const client = await readClientInfo(this.serverUrlHash);
|
|
78985
|
+
if (this.configuredClientId && client?.issuer && client.issuer !== issuer && client.client_id === this.configuredClientId) {
|
|
78986
|
+
throw new OAuthError2(
|
|
78987
|
+
"Configured client belongs to another authorization server",
|
|
78988
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
78989
|
+
);
|
|
78990
|
+
}
|
|
78991
|
+
this.config.scopes = [
|
|
78992
|
+
.../* @__PURE__ */ new Set([
|
|
78993
|
+
...stored?.issuer === issuer ? stored.scope?.split(" ").filter(Boolean) ?? [] : [],
|
|
78994
|
+
...this.config.scopes,
|
|
78995
|
+
...request.scopes ?? []
|
|
78996
|
+
])
|
|
78997
|
+
];
|
|
78998
|
+
const needsScope = !!request.scopes?.some(
|
|
78999
|
+
(scope) => !stored?.scope?.split(" ").includes(scope)
|
|
79000
|
+
);
|
|
79001
|
+
const clientId = this.configuredClientId || client?.client_id;
|
|
79002
|
+
if (stored?.issuer === issuer && client?.issuer === issuer && stored.refresh_token && clientId && (!this.configuredClientId || this.configuredClientId === client.client_id) && !needsScope) {
|
|
79003
|
+
try {
|
|
79004
|
+
const renewed = await refreshAuthorization(issuer, {
|
|
79005
|
+
metadata: { ...this.authServerMetadata },
|
|
79006
|
+
clientInformation: { client_id: clientId },
|
|
79007
|
+
refreshToken: stored.refresh_token,
|
|
79008
|
+
resource: this.resourceIndicator(),
|
|
79009
|
+
fetchFn: (url2, init) => oauthFetch(String(url2), init)
|
|
79010
|
+
});
|
|
79011
|
+
if (renewed.token_type.toLowerCase() !== "bearer")
|
|
79012
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
79013
|
+
const tokens = {
|
|
79014
|
+
...renewed,
|
|
79015
|
+
issuer,
|
|
79016
|
+
scope: renewed.scope ?? stored.scope,
|
|
79017
|
+
obtained_at: Date.now()
|
|
79018
|
+
};
|
|
79019
|
+
await writeTokens(this.serverUrlHash, tokens);
|
|
79020
|
+
return tokens;
|
|
79021
|
+
} catch (error51) {
|
|
79022
|
+
if (!(error51 instanceof InvalidGrantError)) throw error51;
|
|
79023
|
+
await deleteConfigFile(this.serverUrlHash, "tokens.json");
|
|
79024
|
+
}
|
|
79025
|
+
}
|
|
79026
|
+
return await this.performAuthorization();
|
|
79072
79027
|
} finally {
|
|
79073
|
-
|
|
79028
|
+
await release();
|
|
79074
79029
|
}
|
|
79075
79030
|
}
|
|
79031
|
+
resourceIndicator() {
|
|
79032
|
+
if (!CONFIG.OAUTH_RESOURCE_INDICATOR)
|
|
79033
|
+
throw new OAuthError2(
|
|
79034
|
+
"MCP OAuth requires resource indicators; remove OAUTH_RESOURCE_INDICATOR=false",
|
|
79035
|
+
"RESOURCE_REQUIRED"
|
|
79036
|
+
);
|
|
79037
|
+
return this.config.resource;
|
|
79038
|
+
}
|
|
79076
79039
|
/**
|
|
79077
79040
|
* Perform the complete OAuth 2.1 authorization flow
|
|
79078
79041
|
*/
|
|
79079
79042
|
async performAuthorization() {
|
|
79080
79043
|
try {
|
|
79081
|
-
await this.discoverOAuthEndpoints();
|
|
79082
|
-
|
|
79044
|
+
if (!this.authServerMetadata) await this.discoverOAuthEndpoints();
|
|
79045
|
+
if (!this.authServerMetadata?.code_challenge_methods_supported?.includes("S256"))
|
|
79046
|
+
throw new OAuthError2(
|
|
79047
|
+
"Authorization server must advertise PKCE S256 support",
|
|
79048
|
+
"PKCE_REQUIRED"
|
|
79049
|
+
);
|
|
79050
|
+
this.resourceIndicator();
|
|
79051
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(this.config.host))
|
|
79052
|
+
throw new OAuthError2(
|
|
79053
|
+
"Native OAuth callback must bind to loopback",
|
|
79054
|
+
"INVALID_CALLBACK_HOST"
|
|
79055
|
+
);
|
|
79056
|
+
this.currentPKCE = generatePKCE();
|
|
79057
|
+
this.currentState = generateSecureState();
|
|
79058
|
+
const callbackPort = this.config.callbackPort === 0 ? await selectCallbackPort(this.config.serverUrl, void 0, false, this.config.host) : this.config.callbackPort;
|
|
79083
79059
|
const callbackServer = setupWPOAuthCallbackServer(
|
|
79084
79060
|
{
|
|
79085
79061
|
port: callbackPort,
|
|
79086
79062
|
host: this.config.host,
|
|
79087
79063
|
serverUrlHash: this.serverUrlHash,
|
|
79088
|
-
timeout: this.config.timeout
|
|
79064
|
+
timeout: this.config.timeout,
|
|
79065
|
+
state: this.currentState,
|
|
79066
|
+
issuer: this.authServerMetadata.issuer,
|
|
79067
|
+
requireIssuer: this.authServerMetadata.authorization_response_iss_parameter_supported
|
|
79089
79068
|
},
|
|
79090
79069
|
this.events
|
|
79091
79070
|
);
|
|
79092
79071
|
await callbackServer.start();
|
|
79093
79072
|
logger.oauth("OAuth callback server started");
|
|
79094
79073
|
try {
|
|
79095
|
-
const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
|
|
79074
|
+
const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
|
|
79096
79075
|
this.config.redirectUri = actualRedirectUri;
|
|
79097
79076
|
await this.ensureClientRegistration();
|
|
79098
|
-
this.currentPKCE = generatePKCE();
|
|
79099
|
-
this.currentState = generateSecureState();
|
|
79100
79077
|
await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
|
|
79101
79078
|
await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
|
|
79102
79079
|
const authUrl = buildAuthorizationUrl(
|
|
@@ -79106,10 +79083,13 @@ var MCPOAuthProvider = class {
|
|
|
79106
79083
|
this.config.scopes,
|
|
79107
79084
|
this.currentState,
|
|
79108
79085
|
this.currentPKCE.codeChallenge,
|
|
79109
|
-
|
|
79086
|
+
this.resourceIndicator()
|
|
79110
79087
|
);
|
|
79111
79088
|
logger.oauth("Built OAuth 2.1 authorization URL");
|
|
79112
79089
|
logger.debug("Authorization URL", "OAUTH", { url: authUrl });
|
|
79090
|
+
const authorizationCode = this.waitForAuthorizationCode();
|
|
79091
|
+
void authorizationCode.catch(() => {
|
|
79092
|
+
});
|
|
79113
79093
|
try {
|
|
79114
79094
|
await open_default(authUrl);
|
|
79115
79095
|
logger.oauth("Browser opened successfully");
|
|
@@ -79120,13 +79100,20 @@ var MCPOAuthProvider = class {
|
|
|
79120
79100
|
logger.info(`${authUrl}`);
|
|
79121
79101
|
logger.info("===============================\n");
|
|
79122
79102
|
}
|
|
79123
|
-
const authCode = await
|
|
79124
|
-
const
|
|
79103
|
+
const authCode = await authorizationCode;
|
|
79104
|
+
const response = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
|
|
79105
|
+
const tokens = {
|
|
79106
|
+
...response,
|
|
79107
|
+
issuer: this.getAuthorizationServerUrl(),
|
|
79108
|
+
scope: response.scope ?? this.config.scopes.join(" ")
|
|
79109
|
+
};
|
|
79125
79110
|
await writeTokens(this.serverUrlHash, tokens);
|
|
79126
79111
|
logger.oauth("OAuth 2.1 tokens stored successfully");
|
|
79127
79112
|
return tokens;
|
|
79128
79113
|
} finally {
|
|
79129
79114
|
await callbackServer.stop();
|
|
79115
|
+
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
79116
|
+
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
79130
79117
|
}
|
|
79131
79118
|
} catch (error51) {
|
|
79132
79119
|
logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
|
|
@@ -79147,12 +79134,16 @@ var MCPOAuthProvider = class {
|
|
|
79147
79134
|
this.events.removeAllListeners("oauth-error");
|
|
79148
79135
|
clearTimeout(timeout);
|
|
79149
79136
|
};
|
|
79150
|
-
this.events.once("oauth-code-received", async ({ code, state }) => {
|
|
79137
|
+
this.events.once("oauth-code-received", async ({ code, state, iss }) => {
|
|
79151
79138
|
cleanup();
|
|
79152
79139
|
if (state !== this.currentState) {
|
|
79153
79140
|
reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
|
|
79154
79141
|
return;
|
|
79155
79142
|
}
|
|
79143
|
+
if ((iss !== void 0 || this.authServerMetadata?.authorization_response_iss_parameter_supported) && iss !== this.authServerMetadata?.issuer) {
|
|
79144
|
+
reject(new OAuthError2("Authorization response issuer mismatch", "INVALID_ISSUER"));
|
|
79145
|
+
return;
|
|
79146
|
+
}
|
|
79156
79147
|
logger.oauth("Valid authorization code received");
|
|
79157
79148
|
resolve(code);
|
|
79158
79149
|
});
|
|
@@ -79178,7 +79169,7 @@ var MCPOAuthProvider = class {
|
|
|
79178
79169
|
redirectUri,
|
|
79179
79170
|
this.config.clientId,
|
|
79180
79171
|
codeVerifier,
|
|
79181
|
-
|
|
79172
|
+
this.resourceIndicator()
|
|
79182
79173
|
);
|
|
79183
79174
|
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
79184
79175
|
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
@@ -79244,6 +79235,7 @@ import * as path5 from "node:path";
|
|
|
79244
79235
|
import * as os3 from "node:os";
|
|
79245
79236
|
|
|
79246
79237
|
// src/lib/persistent-oauth-client-provider.ts
|
|
79238
|
+
import { randomBytes } from "node:crypto";
|
|
79247
79239
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
79248
79240
|
var PersistentWPOAuthClientProvider = class {
|
|
79249
79241
|
constructor(options) {
|
|
@@ -79401,7 +79393,10 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
79401
79393
|
*/
|
|
79402
79394
|
async performAuthorization() {
|
|
79403
79395
|
const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
|
|
79396
|
+
const state = this.generateState();
|
|
79404
79397
|
const callbackServerOptions = {
|
|
79398
|
+
flow: "implicit",
|
|
79399
|
+
state,
|
|
79405
79400
|
port: callbackPort,
|
|
79406
79401
|
host: this.options.host,
|
|
79407
79402
|
serverUrlHash: this.serverUrlHash,
|
|
@@ -79413,12 +79408,14 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
79413
79408
|
logger.oauth("Starting callback server...");
|
|
79414
79409
|
await callbackServer.start();
|
|
79415
79410
|
logger.oauth("Callback server started successfully");
|
|
79416
|
-
const state = this.generateState();
|
|
79417
79411
|
logger.debug(`Generated state parameter: ${state}`, "OAUTH");
|
|
79418
79412
|
const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
|
|
79419
79413
|
logger.oauth(`Built authorization URL: ${authUrl}`);
|
|
79420
79414
|
logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
|
|
79421
79415
|
logger.oauth("Attempting to open browser...");
|
|
79416
|
+
const authorization = this.waitForAuthorizationResult();
|
|
79417
|
+
void authorization.catch(() => {
|
|
79418
|
+
});
|
|
79422
79419
|
try {
|
|
79423
79420
|
await open_default(authUrl);
|
|
79424
79421
|
logger.oauth("Browser opened successfully");
|
|
@@ -79430,7 +79427,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
79430
79427
|
logger.info("===============================\n");
|
|
79431
79428
|
}
|
|
79432
79429
|
logger.oauth("Waiting for authorization result...");
|
|
79433
|
-
const tokens = await
|
|
79430
|
+
const tokens = await authorization;
|
|
79434
79431
|
logger.oauth("Authorization result received");
|
|
79435
79432
|
logger.oauth("Saving tokens to persistent storage...");
|
|
79436
79433
|
await this.saveTokens(tokens);
|
|
@@ -79483,7 +79480,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
79483
79480
|
* Generate a secure state parameter
|
|
79484
79481
|
*/
|
|
79485
79482
|
generateState() {
|
|
79486
|
-
return
|
|
79483
|
+
return randomBytes(32).toString("base64url");
|
|
79487
79484
|
}
|
|
79488
79485
|
/**
|
|
79489
79486
|
* Wait for authorization result from callback server
|
|
@@ -79878,6 +79875,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
|
|
|
79878
79875
|
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
79879
79876
|
}
|
|
79880
79877
|
}
|
|
79878
|
+
function getMCPOAuthProvider() {
|
|
79879
|
+
return mcpOAuthProvider ??= new MCPOAuthProvider({
|
|
79880
|
+
serverUrl: getRequestUrl(),
|
|
79881
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID
|
|
79882
|
+
});
|
|
79883
|
+
}
|
|
79881
79884
|
async function getOAuthTokens() {
|
|
79882
79885
|
try {
|
|
79883
79886
|
if (!CONFIG.OAUTH_ENABLED) {
|
|
@@ -79885,7 +79888,7 @@ async function getOAuthTokens() {
|
|
|
79885
79888
|
return null;
|
|
79886
79889
|
}
|
|
79887
79890
|
logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
|
|
79888
|
-
const serverUrl =
|
|
79891
|
+
const serverUrl = getRequestUrl();
|
|
79889
79892
|
const serverUrlHash = generateServerUrlHash(serverUrl);
|
|
79890
79893
|
const existingTokens = await getValidTokens(serverUrlHash);
|
|
79891
79894
|
if (existingTokens) {
|
|
@@ -79896,16 +79899,10 @@ async function getOAuthTokens() {
|
|
|
79896
79899
|
logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
|
|
79897
79900
|
logger.auth("Your browser should open automatically for authentication");
|
|
79898
79901
|
if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE) {
|
|
79899
|
-
if (!mcpOAuthProvider) {
|
|
79900
|
-
mcpOAuthProvider = new MCPOAuthProvider({
|
|
79901
|
-
serverUrl,
|
|
79902
|
-
clientId: CONFIG.WP_OAUTH_CLIENT_ID,
|
|
79903
|
-
scopes: getDefaultOAuthScopes()
|
|
79904
|
-
});
|
|
79905
|
-
}
|
|
79906
79902
|
logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
|
|
79907
|
-
|
|
79908
|
-
|
|
79903
|
+
const provider = getMCPOAuthProvider();
|
|
79904
|
+
await provider.authorize();
|
|
79905
|
+
const tokens = await provider.tokens();
|
|
79909
79906
|
if (tokens) {
|
|
79910
79907
|
logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
|
|
79911
79908
|
return tokens;
|
|
@@ -79946,17 +79943,20 @@ async function getOAuthTokens() {
|
|
|
79946
79943
|
return null;
|
|
79947
79944
|
}
|
|
79948
79945
|
}
|
|
79949
|
-
async function
|
|
79950
|
-
if (!CONFIG.OAUTH_ENABLED || CONFIG.JWT_TOKEN)
|
|
79946
|
+
async function recoverOAuthAuthorization(authorization, challenge, status = 401) {
|
|
79947
|
+
if (!CONFIG.OAUTH_ENABLED || CONFIG.JWT_TOKEN || CONFIG.OAUTH_FLOW_TYPE !== "authorization_code" || !CONFIG.OAUTH_USE_PKCE)
|
|
79951
79948
|
return false;
|
|
79952
|
-
|
|
79953
|
-
const
|
|
79954
|
-
|
|
79955
|
-
|
|
79956
|
-
|
|
79957
|
-
|
|
79958
|
-
|
|
79959
|
-
|
|
79949
|
+
if (authorization && !/^Bearer /i.test(authorization)) return false;
|
|
79950
|
+
const scopes = status === 401 || challenge?.error === "insufficient_scope" ? challenge?.scope?.split(/\s+/).filter(Boolean) : void 0;
|
|
79951
|
+
if (status === 403 && !scopes?.length) return false;
|
|
79952
|
+
const current = await getValidTokens(generateServerUrlHash(getRequestUrl()));
|
|
79953
|
+
if (status === 403 && scopes?.every((scope) => current?.scope?.split(" ").includes(scope)))
|
|
79954
|
+
return current?.access_token !== authorization?.slice(7);
|
|
79955
|
+
await getMCPOAuthProvider().authorize({
|
|
79956
|
+
rejectedToken: status === 401 ? authorization?.slice(7) : void 0,
|
|
79957
|
+
challenge,
|
|
79958
|
+
scopes
|
|
79959
|
+
});
|
|
79960
79960
|
return true;
|
|
79961
79961
|
}
|
|
79962
79962
|
function getCurrentApiUrl() {
|
|
@@ -79977,7 +79977,7 @@ function bindWordPressRequest(request) {
|
|
|
79977
79977
|
if (shared[LIVE_REQUEST_KEY] === request) delete shared[LIVE_REQUEST_KEY];
|
|
79978
79978
|
};
|
|
79979
79979
|
}
|
|
79980
|
-
async function getWordPressHeaders(requestData) {
|
|
79980
|
+
async function getWordPressHeaders(requestData, options = {}) {
|
|
79981
79981
|
validateEnvironment();
|
|
79982
79982
|
let authHeader = "";
|
|
79983
79983
|
if (CONFIG.JWT_TOKEN) {
|
|
@@ -79986,7 +79986,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
79986
79986
|
logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
|
|
79987
79987
|
} else if (CONFIG.OAUTH_ENABLED) {
|
|
79988
79988
|
logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
|
|
79989
|
-
const oauthTokens = await getOAuthTokens();
|
|
79989
|
+
const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
|
|
79990
79990
|
if (oauthTokens) {
|
|
79991
79991
|
authHeader = `Bearer ${oauthTokens.access_token}`;
|
|
79992
79992
|
logger.auth("Using OAuth token authentication for WordPress API");
|
|
@@ -80026,7 +80026,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
80026
80026
|
if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
|
|
80027
80027
|
}
|
|
80028
80028
|
const hasCustomHeaders = Object.keys(customHeaders).length > 0;
|
|
80029
|
-
if (!authHeader && !hasCustomHeaders) {
|
|
80029
|
+
if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
|
|
80030
80030
|
throw new AuthError(
|
|
80031
80031
|
"No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
|
|
80032
80032
|
"NO_AUTH_METHOD"
|
|
@@ -80059,7 +80059,7 @@ function collectHeaderParams(schema, path6 = []) {
|
|
|
80059
80059
|
});
|
|
80060
80060
|
}
|
|
80061
80061
|
function isBearerChallenge(challenge) {
|
|
80062
|
-
return challenge === null ||
|
|
80062
|
+
return challenge === null || parseWWWAuthenticateHeader(challenge).scheme === "Bearer";
|
|
80063
80063
|
}
|
|
80064
80064
|
function valueAtPath(value, path6) {
|
|
80065
80065
|
return path6.reduce(
|
|
@@ -80070,6 +80070,8 @@ function valueAtPath(value, path6) {
|
|
|
80070
80070
|
function createWordPressTransport(onFailure) {
|
|
80071
80071
|
const inflight = /* @__PURE__ */ new Map();
|
|
80072
80072
|
const toolHeaders = /* @__PURE__ */ new Map();
|
|
80073
|
+
const scopeAttempts = /* @__PURE__ */ new Set();
|
|
80074
|
+
const scopeCounts = /* @__PURE__ */ new Map();
|
|
80073
80075
|
const send = async (url2, init, retried) => {
|
|
80074
80076
|
const message = typeof init?.body === "string" ? JSON.parse(init.body) : void 0;
|
|
80075
80077
|
const request = message && "method" in message && "id" in message ? message : void 0;
|
|
@@ -80103,7 +80105,9 @@ function createWordPressTransport(onFailure) {
|
|
|
80103
80105
|
return apiError;
|
|
80104
80106
|
};
|
|
80105
80107
|
try {
|
|
80106
|
-
const headers = new Headers(
|
|
80108
|
+
const headers = new Headers(
|
|
80109
|
+
await getWordPressHeaders(message, { allowUnauthenticated: !retried })
|
|
80110
|
+
);
|
|
80107
80111
|
if (init?.method !== "GET" && request?.method !== "subscriptions/listen") {
|
|
80108
80112
|
timer = setTimeout(() => {
|
|
80109
80113
|
timedOut = true;
|
|
@@ -80136,13 +80140,40 @@ function createWordPressTransport(onFailure) {
|
|
|
80136
80140
|
headers,
|
|
80137
80141
|
signal: controller.signal
|
|
80138
80142
|
});
|
|
80139
|
-
|
|
80140
|
-
|
|
80141
|
-
|
|
80142
|
-
|
|
80143
|
-
|
|
80144
|
-
|
|
80145
|
-
|
|
80143
|
+
const challenge = parseWWWAuthenticateHeader(response.headers.get("WWW-Authenticate") ?? "");
|
|
80144
|
+
const operationKey = JSON.stringify([
|
|
80145
|
+
request?.method,
|
|
80146
|
+
request?.params?.name ?? request?.params?.uri
|
|
80147
|
+
]);
|
|
80148
|
+
const scopeKey = JSON.stringify([
|
|
80149
|
+
request?.method,
|
|
80150
|
+
request?.params?.name,
|
|
80151
|
+
challenge.scope?.split(/\s+/).sort()
|
|
80152
|
+
]);
|
|
80153
|
+
const canRecover = !retried && (response.status === 401 && isBearerChallenge(response.headers.get("WWW-Authenticate")) || response.status === 403 && challenge.error === "insufficient_scope" && !!challenge.scope && !scopeAttempts.has(scopeKey) && (scopeCounts.get(operationKey) ?? 0) < 2);
|
|
80154
|
+
if (canRecover) {
|
|
80155
|
+
if (response.status === 403) {
|
|
80156
|
+
scopeAttempts.add(scopeKey);
|
|
80157
|
+
scopeCounts.set(operationKey, (scopeCounts.get(operationKey) ?? 0) + 1);
|
|
80158
|
+
}
|
|
80159
|
+
clearTimeout(timer);
|
|
80160
|
+
const recovered = await recoverOAuthAuthorization(
|
|
80161
|
+
headers.get("Authorization"),
|
|
80162
|
+
challenge,
|
|
80163
|
+
response.status
|
|
80164
|
+
);
|
|
80165
|
+
if (recovered) {
|
|
80166
|
+
const unread = response.body;
|
|
80167
|
+
if (typeof unread?.cancel === "function") await unread.cancel();
|
|
80168
|
+
else unread?.destroy?.();
|
|
80169
|
+
cleanup();
|
|
80170
|
+
if (cancelled) return new Response(null, { status: 202 });
|
|
80171
|
+
return send(url2, init, true);
|
|
80172
|
+
}
|
|
80173
|
+
timer = setTimeout(() => {
|
|
80174
|
+
timedOut = true;
|
|
80175
|
+
controller.abort();
|
|
80176
|
+
}, timeoutMs);
|
|
80146
80177
|
}
|
|
80147
80178
|
const body = response.body && typeof response.body.getReader !== "function" ? Readable.toWeb(response.body) : response.body;
|
|
80148
80179
|
const reader = body?.getReader();
|
|
@@ -80432,11 +80463,6 @@ async function startPassThrough(local, remote) {
|
|
|
80432
80463
|
validateNodeVersion(18);
|
|
80433
80464
|
async function WordPressProxy() {
|
|
80434
80465
|
await setupFetchPolyfill();
|
|
80435
|
-
try {
|
|
80436
|
-
await cleanupExpiredTokens();
|
|
80437
|
-
} catch (error51) {
|
|
80438
|
-
logger.warn("Error cleaning up expired tokens", "PROXY", error51);
|
|
80439
|
-
}
|
|
80440
80466
|
const local = new StdioServerTransport();
|
|
80441
80467
|
let bridge;
|
|
80442
80468
|
const remote = createWordPressTransport((message, error51) => bridge?.fail(message, error51));
|