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