@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/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 writeJsonFile(serverUrlHash, "tokens.json", tokensWithTimestamp);
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 (!tokens.expires_in || !tokens.obtained_at) {
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 defaultPort = calculateDefaultPort(serverUrlHash);
76635
- const [existingClientPort, availablePort] = await Promise.all([
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.2";
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
- // Always treat as self-hosted for port selection
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 delay = this._getNextReconnectionDelay(attemptCount);
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
- }, delay);
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
- try {
78327
- const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationServerUrl);
78328
- logger.oauth(`Discovering authorization server metadata: ${metadataUrl}`);
78329
- const customHeaders = getCustomHeaders();
78330
- const response = await proxyFetch(metadataUrl.toString(), {
78331
- headers: {
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
- `Failed to fetch authorization server metadata: ${response.status}`,
78343
+ `Metadata request failed (${response.status})`,
78339
78344
  "METADATA_DISCOVERY_FAILED"
78340
78345
  );
78341
- }
78342
- const metadata = await response.json();
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 server metadata missing required endpoints",
78349
+ "Authorization metadata has an invalid or mismatched issuer",
78346
78350
  "INVALID_METADATA"
78347
78351
  );
78348
78352
  }
78349
- logger.oauth("Authorization server metadata discovered successfully");
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
- try {
78363
- const url2 = new URL(resourceUrl);
78364
- const metadataUrl = new URL("/.well-known/oauth-protected-resource", url2.origin);
78365
- logger.oauth(`Discovering protected resource metadata: ${metadataUrl}`);
78366
- const customHeaders = getCustomHeaders();
78367
- const response = await proxyFetch(metadataUrl.toString(), {
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
- if (!response.ok) {
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
- `Failed to fetch protected resource metadata: ${response.status}`,
78381
+ `Resource metadata request failed (${response.status})`,
78376
78382
  "RESOURCE_METADATA_DISCOVERY_FAILED"
78377
78383
  );
78378
- }
78379
- const metadata = await response.json();
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 missing authorization servers",
78387
+ "Protected resource metadata has an invalid or mismatched resource",
78383
78388
  "INVALID_RESOURCE_METADATA"
78384
78389
  );
78385
78390
  }
78386
- logger.oauth("Protected resource metadata discovered successfully");
78387
- return metadata;
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 parts = headerValue2.split(/\s+/);
78400
- const scheme = parts[0];
78401
- const result = { scheme };
78402
- const paramString = parts.slice(1).join(" ");
78403
- const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
78404
- for (const match of paramMatches) {
78405
- const [, key, value] = match;
78406
- switch (key) {
78407
- case "realm":
78408
- result.realm = value;
78409
- break;
78410
- case "scope":
78411
- result.scope = value;
78412
- break;
78413
- case "error":
78414
- result.error = value;
78415
- break;
78416
- case "error_description":
78417
- result.error_description = value;
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 customHeaders = getCustomHeaders();
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 customHeaders = getCustomHeaders();
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
- return `${authorizationEndpoint}?${params.toString()}`;
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
- function displayStatus(message, type = 'loading') {
78619
- const statusEl = document.getElementById('status');
78620
- const spinnerEl = document.getElementById('spinner');
78621
-
78622
- statusEl.textContent = message;
78623
- statusEl.className = type;
78624
-
78625
- if (type !== 'loading') {
78626
- spinnerEl.style.display = 'none';
78627
- }
78628
- }
78629
-
78630
- function displayDetails(message) {
78631
- document.getElementById('details').textContent = message;
78632
- }
78633
-
78634
- // Handle both OAuth 2.1 authorization code flow and implicit flow
78635
- const urlParams = new URLSearchParams(window.location.search);
78636
- const hashParams = new URLSearchParams(window.location.hash.substring(1));
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
- if (!code) {
78745
- throw new OAuthError2("No authorization code received");
78746
- }
78747
- logger.oauth("OAuth 2.1 authorization code received");
78748
- logger.debug("Authorization code details", "OAUTH", {
78749
- codeLength: code.length,
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({ success: true, message: "Authorization code received successfully" });
78686
+ res.json({
78687
+ success: true,
78688
+ message: "Authorization code received; check connection status in the app"
78689
+ });
78755
78690
  } catch (error51) {
78756
- logger.error("Error processing authorization code", "OAUTH", error51);
78757
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
78758
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
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
- const tokens = req.body;
78765
- if (!tokens.access_token) {
78766
- throw new OAuthError2("No access token received");
78767
- }
78768
- logger.oauth("Legacy OAuth tokens received (implicit flow)");
78769
- logger.debug("Token details", "OAUTH", {
78770
- tokenType: tokens.token_type,
78771
- expiresIn: tokens.expires_in,
78772
- scope: tokens.scope,
78773
- tokenLength: tokens.access_token.length
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, message: "Tokens saved successfully" });
78714
+ res.json({ success: true });
78778
78715
  } catch (error51) {
78779
- logger.error("Error processing OAuth tokens", "OAUTH", error51);
78780
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
78781
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
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
- try {
78921
- logger.oauth("Starting MCP-compliant OAuth endpoint discovery");
78922
- try {
78923
- this.resourceMetadata = await discoverProtectedResourceMetadata(this.config.resource);
78924
- logger.oauth("Protected resource metadata discovered");
78925
- } catch (error51) {
78926
- logger.warn(
78927
- "Protected resource metadata discovery failed, trying 401 response method",
78928
- "OAUTH"
78929
- );
78930
- await this.discoverVia401Response();
78931
- }
78932
- const authServerUrl = this.getAuthorizationServerUrl();
78933
- if (authServerUrl) {
78934
- this.authServerMetadata = await discoverAuthorizationServerMetadata2(authServerUrl);
78935
- logger.oauth("Authorization server metadata discovered");
78936
- this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
78937
- this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
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
- if (this.resourceMetadata?.authorization_servers?.length) {
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
- logger.oauth("Using provided client ID, skipping dynamic registration");
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
- const storedClientInfo = await readClientInfo(this.serverUrlHash);
79006
- if (typeof storedClientInfo?.client_id === "string" && storedClientInfo.client_id.length > 0) {
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 ID provided and dynamic registration is disabled",
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
- "Dynamic client registration not supported by authorization server",
78916
+ "Authorization server does not support dynamic registration; configure its client ID",
79020
78917
  "NO_DYNAMIC_REGISTRATION"
79021
78918
  );
79022
- }
79023
- try {
79024
- logger.oauth("Performing dynamic client registration");
79025
- const registrationRequest = {
79026
- redirect_uris: [this.config.redirectUri],
79027
- token_endpoint_auth_method: "none",
79028
- // Public client
79029
- grant_types: ["authorization_code"],
79030
- response_types: ["code"],
79031
- client_name: "WordPress MCP Remote",
79032
- scope: this.config.scopes.join(" ")
79033
- };
79034
- const registrationResponse = await registerDynamicClient(
79035
- this.authServerMetadata.registration_endpoint,
79036
- registrationRequest
79037
- );
79038
- const clientInfo = {
79039
- client_id: registrationResponse.client_id,
79040
- client_secret: registrationResponse.client_secret
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
- async authorize() {
79054
- if (this.authPromise) {
79055
- logger.oauth("Authorization already in progress, waiting...");
79056
- await this.authPromise;
79057
- return;
79058
- }
79059
- const existingTokens = await this.tokens();
79060
- if (existingTokens) {
79061
- logger.oauth("Already have valid tokens, skipping authorization");
79062
- return;
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
- logger.oauth("Starting MCP-compliant OAuth 2.1 authorization flow");
79065
- this.authPromise = this.performAuthorization();
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 this.authPromise;
79068
- logger.oauth("OAuth 2.1 authorization completed successfully");
78962
+ release = await lockfile.lock(file2, { realpath: false });
79069
78963
  } catch (error51) {
79070
- logger.error("OAuth 2.1 authorization failed", "OAUTH", error51);
79071
- throw error51;
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
- this.authPromise = null;
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
- const callbackPort = this.config.callbackPort === 0 ? await getOAuthCallbackPort() : this.config.callbackPort;
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
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
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 this.waitForAuthorizationCode();
79124
- const tokens = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
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
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
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 this.waitForAuthorizationResult();
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 Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
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 = CONFIG.WP_API_URL;
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
- await mcpOAuthProvider.authorize();
79908
- const tokens = await mcpOAuthProvider.tokens();
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 discardRejectedOAuthToken(authorization) {
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 serverUrlHash = generateServerUrlHash(CONFIG.WP_API_URL);
79954
- const tokens = await readTokens(serverUrlHash);
79955
- if (!tokens || authorization !== `Bearer ${tokens.access_token}`) {
79956
- return false;
79957
- }
79958
- logger.warn("WordPress rejected the stored OAuth token; signing in again", "AUTH");
79959
- await deleteTokens(serverUrlHash);
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 || /^Bearer\b/i.test(challenge);
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(await getWordPressHeaders(message));
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
- if (response.status === 401 && !retried && isBearerChallenge(response.headers.get("WWW-Authenticate")) && await discardRejectedOAuthToken(headers.get("Authorization"))) {
80140
- const unread = response.body;
80141
- if (typeof unread?.cancel === "function") await unread.cancel();
80142
- else unread?.destroy?.();
80143
- cleanup();
80144
- if (cancelled) return new Response(null, { status: 202 });
80145
- return send(url2, init, true);
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));