@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/lib.js CHANGED
@@ -75554,8 +75554,152 @@ var OAUTH_ERRORS = {
75554
75554
  };
75555
75555
 
75556
75556
  // node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js
75557
+ function isClientAuthMethod(method) {
75558
+ return ["client_secret_basic", "client_secret_post", "none"].includes(method);
75559
+ }
75560
+ function selectClientAuthMethod(clientInformation, supportedMethods) {
75561
+ const hasClientSecret = clientInformation.client_secret !== void 0;
75562
+ if ("token_endpoint_auth_method" in clientInformation && clientInformation.token_endpoint_auth_method && isClientAuthMethod(clientInformation.token_endpoint_auth_method) && (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) {
75563
+ return clientInformation.token_endpoint_auth_method;
75564
+ }
75565
+ if (supportedMethods.length === 0) {
75566
+ return hasClientSecret ? "client_secret_basic" : "none";
75567
+ }
75568
+ if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
75569
+ return "client_secret_basic";
75570
+ }
75571
+ if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
75572
+ return "client_secret_post";
75573
+ }
75574
+ if (supportedMethods.includes("none")) {
75575
+ return "none";
75576
+ }
75577
+ return hasClientSecret ? "client_secret_post" : "none";
75578
+ }
75579
+ function applyClientAuthentication(method, clientInformation, headers, params) {
75580
+ const { client_id, client_secret } = clientInformation;
75581
+ switch (method) {
75582
+ case "client_secret_basic":
75583
+ applyBasicAuth(client_id, client_secret, headers);
75584
+ return;
75585
+ case "client_secret_post":
75586
+ applyPostAuth(client_id, client_secret, params);
75587
+ return;
75588
+ case "none":
75589
+ applyPublicAuth(client_id, params);
75590
+ return;
75591
+ default:
75592
+ throw new Error(`Unsupported client authentication method: ${method}`);
75593
+ }
75594
+ }
75595
+ function applyBasicAuth(clientId, clientSecret, headers) {
75596
+ if (!clientSecret) {
75597
+ throw new Error("client_secret_basic authentication requires a client_secret");
75598
+ }
75599
+ const credentials = btoa(`${clientId}:${clientSecret}`);
75600
+ headers.set("Authorization", `Basic ${credentials}`);
75601
+ }
75602
+ function applyPostAuth(clientId, clientSecret, params) {
75603
+ params.set("client_id", clientId);
75604
+ if (clientSecret) {
75605
+ params.set("client_secret", clientSecret);
75606
+ }
75607
+ }
75608
+ function applyPublicAuth(clientId, params) {
75609
+ params.set("client_id", clientId);
75610
+ }
75611
+ async function parseErrorResponse(input) {
75612
+ const statusCode = input instanceof Response ? input.status : void 0;
75613
+ const body = input instanceof Response ? await input.text() : input;
75614
+ try {
75615
+ const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
75616
+ const { error: error51, error_description, error_uri } = result;
75617
+ const errorClass = OAUTH_ERRORS[error51] || ServerError;
75618
+ return new errorClass(error_description || "", error_uri);
75619
+ } catch (error51) {
75620
+ const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error51}. Raw body: ${body}`;
75621
+ return new ServerError(errorMessage);
75622
+ }
75623
+ }
75557
75624
  var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
75558
75625
  var RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true });
75626
+ function buildDiscoveryUrls(authorizationServerUrl) {
75627
+ const url2 = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
75628
+ const hasPath = url2.pathname !== "/";
75629
+ const urlsToTry = [];
75630
+ if (!hasPath) {
75631
+ urlsToTry.push({
75632
+ url: new URL("/.well-known/oauth-authorization-server", url2.origin),
75633
+ type: "oauth"
75634
+ });
75635
+ urlsToTry.push({
75636
+ url: new URL(`/.well-known/openid-configuration`, url2.origin),
75637
+ type: "oidc"
75638
+ });
75639
+ return urlsToTry;
75640
+ }
75641
+ let pathname = url2.pathname;
75642
+ if (pathname.endsWith("/")) {
75643
+ pathname = pathname.slice(0, -1);
75644
+ }
75645
+ urlsToTry.push({
75646
+ url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url2.origin),
75647
+ type: "oauth"
75648
+ });
75649
+ urlsToTry.push({
75650
+ url: new URL(`/.well-known/openid-configuration${pathname}`, url2.origin),
75651
+ type: "oidc"
75652
+ });
75653
+ urlsToTry.push({
75654
+ url: new URL(`${pathname}/.well-known/openid-configuration`, url2.origin),
75655
+ type: "oidc"
75656
+ });
75657
+ return urlsToTry;
75658
+ }
75659
+ function resourceIndicatorToString(resource) {
75660
+ return typeof resource === "string" ? resource : resource.href;
75661
+ }
75662
+ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
75663
+ const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
75664
+ const headers = new Headers({
75665
+ "Content-Type": "application/x-www-form-urlencoded",
75666
+ Accept: "application/json"
75667
+ });
75668
+ if (resource) {
75669
+ tokenRequestParams.set("resource", resourceIndicatorToString(resource));
75670
+ }
75671
+ if (addClientAuthentication) {
75672
+ await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
75673
+ } else if (clientInformation) {
75674
+ const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
75675
+ const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
75676
+ applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
75677
+ }
75678
+ const response = await (fetchFn ?? fetch)(tokenUrl, {
75679
+ method: "POST",
75680
+ headers,
75681
+ body: tokenRequestParams
75682
+ });
75683
+ if (!response.ok) {
75684
+ throw await parseErrorResponse(response);
75685
+ }
75686
+ return TokenResponseSchema.parse(await response.json());
75687
+ }
75688
+ async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
75689
+ const tokenRequestParams = new URLSearchParams({
75690
+ grant_type: "refresh_token",
75691
+ refresh_token: refreshToken
75692
+ });
75693
+ const tokens = await executeTokenRequest(authorizationServerUrl, {
75694
+ metadata,
75695
+ tokenRequestParams,
75696
+ clientInformation,
75697
+ addClientAuthentication,
75698
+ resource,
75699
+ fetchFn
75700
+ });
75701
+ return { refresh_token: refreshToken, ...tokens };
75702
+ }
75559
75703
 
75560
75704
  // src/lib/utils.ts
75561
75705
  import * as fs2 from "node:fs";
@@ -75668,7 +75812,19 @@ async function writeTokens(serverUrlHash, tokens) {
75668
75812
  ...tokens,
75669
75813
  obtained_at: Date.now()
75670
75814
  };
75671
- await writeJsonFile(serverUrlHash, "tokens.json", tokensWithTimestamp);
75815
+ await ensureConfigDir();
75816
+ const file2 = getConfigFilePath(serverUrlHash, "tokens.json");
75817
+ const temporary = `${file2}.${crypto2.randomUUID()}.tmp`;
75818
+ try {
75819
+ await fs.writeFile(temporary, JSON.stringify(tokensWithTimestamp, null, 2), {
75820
+ encoding: "utf-8",
75821
+ mode: 384,
75822
+ flag: "wx"
75823
+ });
75824
+ await fs.rename(temporary, file2);
75825
+ } finally {
75826
+ await fs.rm(temporary, { force: true });
75827
+ }
75672
75828
  logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
75673
75829
  } catch (error51) {
75674
75830
  logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
@@ -75701,7 +75857,7 @@ function isTokenValid(tokens) {
75701
75857
  if (!tokens?.access_token) {
75702
75858
  return { isValid: false, error: "No access token" };
75703
75859
  }
75704
- if (!tokens.expires_in || !tokens.obtained_at) {
75860
+ if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
75705
75861
  return { isValid: true };
75706
75862
  }
75707
75863
  const now = Date.now();
@@ -75740,12 +75896,12 @@ var OAuthClientInfoSchema = external_exports.object({
75740
75896
  client_id: external_exports.string().optional(),
75741
75897
  client_secret: external_exports.string().optional()
75742
75898
  });
75743
- async function findAvailablePort(preferredPort) {
75899
+ async function findAvailablePort(preferredPort, host = "127.0.0.1") {
75744
75900
  return new Promise((resolve, reject) => {
75745
75901
  const server = net.createServer();
75746
75902
  server.on("error", (err) => {
75747
75903
  if (err.code === "EADDRINUSE") {
75748
- server.listen(0);
75904
+ server.listen(0, host);
75749
75905
  } else {
75750
75906
  reject(err);
75751
75907
  }
@@ -75756,7 +75912,7 @@ async function findAvailablePort(preferredPort) {
75756
75912
  resolve(port);
75757
75913
  });
75758
75914
  });
75759
- server.listen(preferredPort || 0);
75915
+ server.listen(preferredPort || 0, host);
75760
75916
  });
75761
75917
  }
75762
75918
  function calculateDefaultPort(serverUrlHash) {
@@ -75784,24 +75940,17 @@ async function findExistingClientPort(serverUrlHash) {
75784
75940
  function getServerUrlHash(serverUrl) {
75785
75941
  return crypto3.createHash("md5").update(serverUrl).digest("hex");
75786
75942
  }
75787
- async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
75943
+ async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
75788
75944
  const serverUrlHash = getServerUrlHash(serverUrl);
75789
75945
  if (specifiedPort) {
75790
75946
  return specifiedPort;
75791
75947
  }
75792
- const defaultPort = calculateDefaultPort(serverUrlHash);
75793
- const [existingClientPort, availablePort] = await Promise.all([
75794
- findExistingClientPort(serverUrlHash),
75795
- findAvailablePort(defaultPort)
75796
- ]);
75797
- if (existingClientPort) {
75798
- return existingClientPort;
75799
- }
75800
- return availablePort;
75948
+ const existingClientPort = await findExistingClientPort(serverUrlHash);
75949
+ return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
75801
75950
  }
75802
75951
 
75803
75952
  // src/lib/config.ts
75804
- var MCP_WORDPRESS_REMOTE_VERSION = "0.5.2";
75953
+ var MCP_WORDPRESS_REMOTE_VERSION = "0.5.3";
75805
75954
  function parsePositiveIntEnv(value, defaultValue) {
75806
75955
  if (!value) {
75807
75956
  return defaultValue;
@@ -76002,8 +76151,8 @@ async function getOAuthCallbackPort() {
76002
76151
  return selectCallbackPort(
76003
76152
  CONFIG.WP_API_URL,
76004
76153
  CONFIG.OAUTH_CALLBACK_PORT,
76005
- false
76006
- // Always treat as self-hosted for port selection
76154
+ false,
76155
+ CONFIG.OAUTH_HOST
76007
76156
  );
76008
76157
  }
76009
76158
  function formatConfigError(error51) {
@@ -76923,10 +77072,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
76923
77072
  var open_default = open;
76924
77073
 
76925
77074
  // src/lib/mcp-oauth-provider.ts
77075
+ import lockfile from "proper-lockfile";
77076
+ import { setTimeout as delay } from "node:timers/promises";
76926
77077
  import { EventEmitter } from "node:events";
76927
77078
 
76928
77079
  // src/lib/mcp-oauth-utils.ts
76929
77080
  import crypto4 from "crypto";
77081
+
77082
+ // src/lib/oauth-http.ts
77083
+ function validateOAuthUrl(value) {
77084
+ let url2;
77085
+ try {
77086
+ url2 = new URL(value);
77087
+ } catch {
77088
+ throw new OAuthError2("Invalid OAuth URL", "INVALID_OAUTH_URL");
77089
+ }
77090
+ const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
77091
+ if (url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback) || url2.username || url2.password || url2.hash) {
77092
+ throw new OAuthError2(
77093
+ "OAuth URLs require HTTPS (HTTP is allowed only on loopback)",
77094
+ "INSECURE_OAUTH_URL"
77095
+ );
77096
+ }
77097
+ return url2;
77098
+ }
77099
+ async function oauthFetch(value, init = {}) {
77100
+ const url2 = validateOAuthUrl(value);
77101
+ const headers = new Headers(
77102
+ url2.origin === new URL(CONFIG.WP_API_URL).origin ? getCustomHeaders() : {}
77103
+ );
77104
+ new Headers(init.headers).forEach((value2, key) => headers.set(key, value2));
77105
+ const response = await proxyFetch(url2.toString(), {
77106
+ ...init,
77107
+ headers,
77108
+ redirect: "error",
77109
+ signal: init.signal ?? AbortSignal.timeout(CONFIG.OAUTH_TIMEOUT)
77110
+ });
77111
+ if (response.status >= 300 && response.status < 400)
77112
+ throw new OAuthError2("OAuth endpoint redirects are not accepted", "OAUTH_REDIRECT");
77113
+ return response;
77114
+ }
77115
+
77116
+ // src/lib/mcp-oauth-utils.ts
76930
77117
  function generatePKCE() {
76931
77118
  const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
76932
77119
  const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
@@ -76954,118 +77141,76 @@ function generateCanonicalResourceURI(serverUrl) {
76954
77141
  }
76955
77142
  }
76956
77143
  async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
76957
- try {
76958
- const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationServerUrl);
76959
- logger.oauth(`Discovering authorization server metadata: ${metadataUrl}`);
76960
- const customHeaders = getCustomHeaders();
76961
- const response = await proxyFetch(metadataUrl.toString(), {
76962
- headers: {
76963
- Accept: "application/json",
76964
- ...customHeaders
76965
- }
76966
- });
76967
- if (!response.ok) {
77144
+ const issuer = validateOAuthUrl(authorizationServerUrl);
77145
+ if (issuer.search) throw new OAuthError2("Issuer must not contain a query", "INVALID_METADATA");
77146
+ for (const { url: url2 } of buildDiscoveryUrls(issuer)) {
77147
+ const response = await oauthFetch(url2.toString(), { headers: { Accept: "application/json" } });
77148
+ if ([404, 405].includes(response.status)) continue;
77149
+ if (!response.ok)
76968
77150
  throw new OAuthError2(
76969
- `Failed to fetch authorization server metadata: ${response.status}`,
77151
+ `Metadata request failed (${response.status})`,
76970
77152
  "METADATA_DISCOVERY_FAILED"
76971
77153
  );
76972
- }
76973
- const metadata = await response.json();
76974
- if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
77154
+ const parsed = OAuthMetadataSchema.safeParse(await response.json());
77155
+ if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
76975
77156
  throw new OAuthError2(
76976
- "Authorization server metadata missing required endpoints",
77157
+ "Authorization metadata has an invalid or mismatched issuer",
76977
77158
  "INVALID_METADATA"
76978
77159
  );
76979
77160
  }
76980
- logger.oauth("Authorization server metadata discovered successfully");
77161
+ const metadata = parsed.data;
77162
+ validateOAuthUrl(metadata.issuer);
77163
+ validateOAuthUrl(metadata.authorization_endpoint);
77164
+ validateOAuthUrl(metadata.token_endpoint);
77165
+ if (metadata.registration_endpoint) validateOAuthUrl(metadata.registration_endpoint);
76981
77166
  return metadata;
76982
- } catch (error51) {
76983
- if (error51 instanceof OAuthError2) {
76984
- throw error51;
76985
- }
76986
- throw new OAuthError2(
76987
- `Error discovering authorization server metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
76988
- "METADATA_DISCOVERY_ERROR"
76989
- );
76990
77167
  }
77168
+ throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
76991
77169
  }
76992
- async function discoverProtectedResourceMetadata(resourceUrl) {
76993
- try {
76994
- const url2 = new URL(resourceUrl);
76995
- const metadataUrl = new URL("/.well-known/oauth-protected-resource", url2.origin);
76996
- logger.oauth(`Discovering protected resource metadata: ${metadataUrl}`);
76997
- const customHeaders = getCustomHeaders();
76998
- const response = await proxyFetch(metadataUrl.toString(), {
76999
- headers: {
77000
- Accept: "application/json",
77001
- ...customHeaders
77002
- }
77003
- });
77004
- if (!response.ok) {
77170
+ async function discoverProtectedResourceMetadata(resourceUrl, resourceMetadataUrl) {
77171
+ const resource = validateOAuthUrl(resourceUrl);
77172
+ const candidates = resourceMetadataUrl ? [{ url: resourceMetadataUrl, expected: resourceUrl }] : [
77173
+ ...resource.pathname !== "/" ? [
77174
+ {
77175
+ url: `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`,
77176
+ expected: resourceUrl
77177
+ }
77178
+ ] : [],
77179
+ {
77180
+ url: `${resource.origin}/.well-known/oauth-protected-resource`,
77181
+ expected: resource.pathname === "/" ? resourceUrl : resource.origin
77182
+ }
77183
+ ];
77184
+ for (const candidate of candidates) {
77185
+ const response = await oauthFetch(candidate.url, { headers: { Accept: "application/json" } });
77186
+ if (!resourceMetadataUrl && [404, 405].includes(response.status)) continue;
77187
+ if (!response.ok)
77005
77188
  throw new OAuthError2(
77006
- `Failed to fetch protected resource metadata: ${response.status}`,
77189
+ `Resource metadata request failed (${response.status})`,
77007
77190
  "RESOURCE_METADATA_DISCOVERY_FAILED"
77008
77191
  );
77009
- }
77010
- const metadata = await response.json();
77011
- if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
77192
+ const parsed = OAuthProtectedResourceMetadataSchema.safeParse(await response.json());
77193
+ if (!parsed.success || parsed.data.resource !== candidate.expected || !parsed.data.authorization_servers?.length) {
77012
77194
  throw new OAuthError2(
77013
- "Protected resource metadata missing authorization servers",
77195
+ "Protected resource metadata has an invalid or mismatched resource",
77014
77196
  "INVALID_RESOURCE_METADATA"
77015
77197
  );
77016
77198
  }
77017
- logger.oauth("Protected resource metadata discovered successfully");
77018
- return metadata;
77019
- } catch (error51) {
77020
- if (error51 instanceof OAuthError2) {
77021
- throw error51;
77022
- }
77023
- throw new OAuthError2(
77024
- `Error discovering protected resource metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
77025
- "RESOURCE_METADATA_DISCOVERY_ERROR"
77026
- );
77199
+ for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
77200
+ return parsed.data;
77027
77201
  }
77028
- }
77029
- function parseWWWAuthenticateHeader(headerValue) {
77030
- const parts = headerValue.split(/\s+/);
77031
- const scheme = parts[0];
77032
- const result = { scheme };
77033
- const paramString = parts.slice(1).join(" ");
77034
- const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
77035
- for (const match of paramMatches) {
77036
- const [, key, value] = match;
77037
- switch (key) {
77038
- case "realm":
77039
- result.realm = value;
77040
- break;
77041
- case "scope":
77042
- result.scope = value;
77043
- break;
77044
- case "error":
77045
- result.error = value;
77046
- break;
77047
- case "error_description":
77048
- result.error_description = value;
77049
- break;
77050
- case "error_uri":
77051
- result.error_uri = value;
77052
- break;
77053
- case "resource_metadata_url":
77054
- result.resource_metadata_url = value;
77055
- break;
77056
- }
77057
- }
77058
- return result;
77202
+ throw new OAuthError2(
77203
+ "No protected resource metadata found",
77204
+ "RESOURCE_METADATA_DISCOVERY_FAILED"
77205
+ );
77059
77206
  }
77060
77207
  async function registerDynamicClient(registrationEndpoint, registrationRequest) {
77061
77208
  try {
77062
77209
  logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
77063
- const customHeaders = getCustomHeaders();
77064
- const response = await proxyFetch(registrationEndpoint, {
77210
+ const response = await oauthFetch(registrationEndpoint, {
77065
77211
  method: "POST",
77066
77212
  headers: {
77067
- "Content-Type": "application/json",
77068
- ...customHeaders
77213
+ "Content-Type": "application/json"
77069
77214
  },
77070
77215
  body: JSON.stringify(registrationRequest)
77071
77216
  });
@@ -77077,12 +77222,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
77077
77222
  );
77078
77223
  }
77079
77224
  const registrationResponse = await response.json();
77080
- if (!registrationResponse.client_id) {
77225
+ if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
77081
77226
  throw new OAuthError2(
77082
77227
  "Dynamic client registration response missing client_id",
77083
77228
  "INVALID_REGISTRATION_RESPONSE"
77084
77229
  );
77085
77230
  }
77231
+ if (registrationResponse.token_endpoint_auth_method && registrationResponse.token_endpoint_auth_method !== registrationRequest.token_endpoint_auth_method) {
77232
+ throw new OAuthError2(
77233
+ "Authorization server changed the requested client authentication method",
77234
+ "INVALID_REGISTRATION_RESPONSE"
77235
+ );
77236
+ }
77237
+ if (registrationResponse.redirect_uris && (!Array.isArray(registrationResponse.redirect_uris) || !registrationRequest.redirect_uris.every(
77238
+ (uri) => registrationResponse.redirect_uris.includes(uri)
77239
+ ))) {
77240
+ throw new OAuthError2(
77241
+ "Authorization server changed the registered callback URI",
77242
+ "INVALID_REGISTRATION_RESPONSE"
77243
+ );
77244
+ }
77086
77245
  logger.oauth("Dynamic client registration successful");
77087
77246
  logger.debug("Client ID obtained", "OAUTH", {
77088
77247
  client_id: registrationResponse.client_id,
@@ -77112,12 +77271,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77112
77271
  if (resource) {
77113
77272
  params.set("resource", resource);
77114
77273
  }
77115
- const customHeaders = getCustomHeaders();
77116
- const response = await proxyFetch(tokenEndpoint, {
77274
+ const response = await oauthFetch(tokenEndpoint, {
77117
77275
  method: "POST",
77118
77276
  headers: {
77119
- "Content-Type": "application/x-www-form-urlencoded",
77120
- ...customHeaders
77277
+ "Content-Type": "application/x-www-form-urlencoded"
77121
77278
  },
77122
77279
  body: params.toString()
77123
77280
  });
@@ -77128,7 +77285,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77128
77285
  "TOKEN_EXCHANGE_FAILED"
77129
77286
  );
77130
77287
  }
77131
- const tokenResponse = await response.json();
77288
+ const tokenResponse = OAuthTokensSchema.parse(await response.json());
77289
+ if (tokenResponse.token_type.toLowerCase() !== "bearer")
77290
+ throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
77132
77291
  if (!tokenResponse.access_token) {
77133
77292
  throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
77134
77293
  }
@@ -77157,15 +77316,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
77157
77316
  // OAuth 2.1 uses authorization code flow
77158
77317
  client_id: clientId,
77159
77318
  redirect_uri: redirectUri,
77160
- scope: scopes.join(" "),
77161
77319
  state,
77162
77320
  code_challenge: codeChallenge,
77163
77321
  code_challenge_method: "S256"
77164
77322
  });
77323
+ if (scopes.length) params.set("scope", scopes.join(" "));
77165
77324
  if (resource) {
77166
77325
  params.set("resource", resource);
77167
77326
  }
77168
- return `${authorizationEndpoint}?${params.toString()}`;
77327
+ validateOAuthUrl(authorizationEndpoint);
77328
+ const url2 = new URL(authorizationEndpoint);
77329
+ params.forEach((value, key) => url2.searchParams.set(key, value));
77330
+ return url2.toString();
77169
77331
  }
77170
77332
  function generateSecureState() {
77171
77333
  return crypto4.randomBytes(32).toString("base64url");
@@ -77246,110 +77408,26 @@ var AUTHORIZATION_CODE_HTML = `
77246
77408
  </div>
77247
77409
 
77248
77410
  <script>
77249
- function displayStatus(message, type = 'loading') {
77250
- const statusEl = document.getElementById('status');
77251
- const spinnerEl = document.getElementById('spinner');
77252
-
77253
- statusEl.textContent = message;
77254
- statusEl.className = type;
77255
-
77256
- if (type !== 'loading') {
77257
- spinnerEl.style.display = 'none';
77258
- }
77259
- }
77260
-
77261
- function displayDetails(message) {
77262
- document.getElementById('details').textContent = message;
77263
- }
77264
-
77265
- // Handle both OAuth 2.1 authorization code flow and implicit flow
77266
- const urlParams = new URLSearchParams(window.location.search);
77267
- const hashParams = new URLSearchParams(window.location.hash.substring(1));
77268
-
77269
- // Check for authorization code first (OAuth 2.1 flow)
77270
- if (urlParams.has('code')) {
77271
- const authData = {
77272
- code: urlParams.get('code'),
77273
- state: urlParams.get('state'),
77274
- // Additional parameters for validation
77275
- iss: urlParams.get('iss'),
77276
- };
77277
-
77278
- // Send authorization code to server for token exchange
77279
- fetch('/oauth/callback', {
77280
- method: 'POST',
77281
- headers: {
77282
- 'Content-Type': 'application/json'
77283
- },
77284
- body: JSON.stringify(authData)
77285
- })
77286
- .then(response => {
77287
- if (response.ok) {
77288
- displayStatus('\u2705 Authorization successful!', 'success');
77289
- displayDetails('You can safely close this window.');
77290
- } else {
77291
- return response.json().then(err => {
77292
- throw new Error(err.error || 'Failed to exchange authorization code');
77293
- });
77294
- }
77295
- })
77296
- .catch(error => {
77297
- displayStatus('\u274C Error completing authorization', 'error');
77298
- displayDetails(error.message + '. Please try again.');
77299
- });
77300
- }
77301
- // Check for access token in URL fragment (implicit flow)
77302
- else if (hashParams.has('access_token')) {
77303
- const tokens = {
77304
- access_token: hashParams.get('access_token'),
77305
- token_type: hashParams.get('token_type') || 'Bearer',
77306
- expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600,
77307
- scope: hashParams.get('scope'),
77308
- state: hashParams.get('state'),
77309
- obtained_at: Date.now()
77310
- };
77311
-
77312
- // Send tokens to server for storage
77313
- fetch('/oauth/tokens', {
77314
- method: 'POST',
77315
- headers: {
77316
- 'Content-Type': 'application/json'
77317
- },
77318
- body: JSON.stringify(tokens)
77319
- })
77320
- .then(response => {
77321
- if (response.ok) {
77322
- displayStatus('\u2705 Authorization successful!', 'success');
77323
- displayDetails('You can safely close this window.');
77324
- } else {
77325
- return response.json().then(err => {
77326
- throw new Error(err.error || 'Failed to store access token');
77327
- });
77328
- }
77329
- })
77330
- .catch(error => {
77331
- displayStatus('\u274C Error completing authorization', 'error');
77332
- displayDetails(error.message + '. Please try again.');
77333
- });
77334
- }
77335
- // Check for errors in query parameters
77336
- else if (urlParams.has('error')) {
77337
- const error = urlParams.get('error');
77338
- const errorDescription = urlParams.get('error_description');
77339
- displayStatus('\u274C Authorization failed', 'error');
77340
- displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
77341
- }
77342
- // Check for errors in URL fragment (implicit flow errors)
77343
- else if (hashParams.has('error')) {
77344
- const error = hashParams.get('error');
77345
- const errorDescription = hashParams.get('error_description');
77346
- displayStatus('\u274C Authorization failed', 'error');
77347
- displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
77348
- }
77349
- // No authorization code or access token found
77350
- else {
77351
- displayStatus('\u274C No authorization code or access token received', 'error');
77352
- displayDetails('Please try the authorization process again.');
77411
+ const query = new URLSearchParams(window.location.search);
77412
+ const fragment = new URLSearchParams(window.location.hash.substring(1));
77413
+ const status = document.getElementById('status');
77414
+ const details = document.getElementById('details');
77415
+ const data = Object.fromEntries(query.has('code') || query.has('error') ? query : fragment);
77416
+ const endpoint = data.access_token ? '/oauth/tokens' : '/oauth/callback';
77417
+ if (data.code || data.error || data.access_token) {
77418
+ fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
77419
+ .then(async response => {
77420
+ const result = await response.json();
77421
+ if (!response.ok) throw new Error(result.error || 'Authorization response was rejected');
77422
+ status.textContent = data.access_token ? 'Authorization successful' : 'Authorization code received';
77423
+ status.className = 'success';
77424
+ details.textContent = data.access_token ? 'You can close this window.' : 'Return to your app to check the connection. Token exchange may still fail.';
77425
+ })
77426
+ .catch(error => { status.textContent = 'Authorization failed'; status.className = 'error'; details.textContent = error.message; })
77427
+ .finally(() => { document.getElementById('spinner').style.display = 'none'; });
77428
+ } else {
77429
+ status.textContent = 'No authorization response received';
77430
+ document.getElementById('spinner').style.display = 'none';
77353
77431
  }
77354
77432
  </script>
77355
77433
  </body>
@@ -77364,6 +77442,17 @@ var OAuthCallbackServer = class {
77364
77442
  this.setupRoutes();
77365
77443
  }
77366
77444
  setupRoutes() {
77445
+ this.app.use((req, res, next) => {
77446
+ const host = this.options.host === "::1" ? "[::1]" : this.options.host;
77447
+ const origin = req.headers.origin;
77448
+ const port = this.server?.address();
77449
+ const expectedOrigin = `http://${host}:${port && typeof port !== "string" ? port.port : this.options.port}`;
77450
+ if (req.hostname !== host || origin && origin !== expectedOrigin) {
77451
+ res.status(403).json({ error: "Untrusted callback origin" });
77452
+ return;
77453
+ }
77454
+ next();
77455
+ });
77367
77456
  this.app.use(import_express.default.json());
77368
77457
  this.app.get("/oauth/callback", (req, res) => {
77369
77458
  logger.oauth("OAuth 2.1 authorization callback page requested");
@@ -77371,46 +77460,47 @@ var OAuthCallbackServer = class {
77371
77460
  });
77372
77461
  this.app.post("/oauth/callback", async (req, res) => {
77373
77462
  try {
77374
- const { code, state, iss } = req.body;
77375
- if (!code) {
77376
- throw new OAuthError2("No authorization code received");
77377
- }
77378
- logger.oauth("OAuth 2.1 authorization code received");
77379
- logger.debug("Authorization code details", "OAUTH", {
77380
- codeLength: code.length,
77381
- state: state || "none",
77382
- issuer: iss || "none"
77383
- });
77463
+ const { code, state, iss, error: error51 } = req.body;
77464
+ this.validateResponse(state, iss);
77465
+ if (error51) throw new OAuthError2("Authorization denied or failed", "AUTH_DENIED");
77466
+ if (this.options.flow === "implicit")
77467
+ throw new OAuthError2("Authorization-code callback is disabled", "WRONG_FLOW");
77468
+ if (typeof code !== "string" || !code)
77469
+ throw new OAuthError2("No authorization code received", "INVALID_CODE");
77384
77470
  this.events.emit("oauth-code-received", { code, state, iss });
77385
- res.json({ success: true, message: "Authorization code received successfully" });
77471
+ res.json({
77472
+ success: true,
77473
+ message: "Authorization code received; check connection status in the app"
77474
+ });
77386
77475
  } catch (error51) {
77387
- logger.error("Error processing authorization code", "OAUTH", error51);
77388
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
77389
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
77390
- res.status(400).json({ error: errorMessage });
77476
+ const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid authorization response");
77477
+ if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
77478
+ res.status(400).json({ error: failure.message });
77391
77479
  }
77392
77480
  });
77393
77481
  this.app.post("/oauth/tokens", async (req, res) => {
77482
+ if (this.options.flow !== "implicit") {
77483
+ res.status(404).json({ error: "Implicit token callback is disabled" });
77484
+ return;
77485
+ }
77394
77486
  try {
77395
- const tokens = req.body;
77396
- if (!tokens.access_token) {
77397
- throw new OAuthError2("No access token received");
77398
- }
77399
- logger.oauth("Legacy OAuth tokens received (implicit flow)");
77400
- logger.debug("Token details", "OAUTH", {
77401
- tokenType: tokens.token_type,
77402
- expiresIn: tokens.expires_in,
77403
- scope: tokens.scope,
77404
- tokenLength: tokens.access_token.length
77405
- });
77487
+ this.validateResponse(req.body.state, req.body.iss);
77488
+ if (typeof req.body.access_token !== "string" || !req.body.access_token || req.body.token_type && req.body.token_type.toLowerCase() !== "bearer")
77489
+ throw new OAuthError2("Invalid implicit token response");
77490
+ const tokens = {
77491
+ access_token: req.body.access_token,
77492
+ token_type: "Bearer",
77493
+ expires_in: req.body.expires_in === void 0 ? void 0 : Number(req.body.expires_in),
77494
+ scope: req.body.scope,
77495
+ obtained_at: Date.now()
77496
+ };
77406
77497
  await writeTokens(this.options.serverUrlHash, tokens);
77407
77498
  this.events.emit("oauth-success", tokens);
77408
- res.json({ success: true, message: "Tokens saved successfully" });
77499
+ res.json({ success: true });
77409
77500
  } catch (error51) {
77410
- logger.error("Error processing OAuth tokens", "OAUTH", error51);
77411
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
77412
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
77413
- res.status(400).json({ error: errorMessage });
77501
+ const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid implicit response");
77502
+ if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
77503
+ res.status(400).json({ error: failure.message });
77414
77504
  }
77415
77505
  });
77416
77506
  this.app.get("/oauth/health", (req, res) => {
@@ -77427,10 +77517,17 @@ var OAuthCallbackServer = class {
77427
77517
  }
77428
77518
  );
77429
77519
  }
77520
+ validateResponse(state, issuer) {
77521
+ if (!this.options.state || state !== this.options.state)
77522
+ throw new OAuthError2("Invalid callback state", "INVALID_STATE");
77523
+ if ((issuer !== void 0 || this.options.requireIssuer) && issuer !== this.options.issuer)
77524
+ throw new OAuthError2("Invalid callback issuer", "INVALID_ISSUER");
77525
+ }
77430
77526
  async start() {
77431
77527
  return new Promise((resolve, reject) => {
77432
77528
  try {
77433
77529
  this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
77530
+ clearTimeout(this.startupTimer);
77434
77531
  if (error51) {
77435
77532
  reject(error51);
77436
77533
  return;
@@ -77441,11 +77538,12 @@ var OAuthCallbackServer = class {
77441
77538
  resolve();
77442
77539
  });
77443
77540
  this.server.on("error", (error51) => {
77541
+ clearTimeout(this.startupTimer);
77444
77542
  logger.error("OAuth callback server error", "OAUTH", error51);
77445
77543
  reject(error51);
77446
77544
  });
77447
77545
  if (this.options.timeout) {
77448
- setTimeout(() => {
77546
+ this.startupTimer = setTimeout(() => {
77449
77547
  if (!this.server?.listening) {
77450
77548
  reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
77451
77549
  }
@@ -77457,6 +77555,7 @@ var OAuthCallbackServer = class {
77457
77555
  });
77458
77556
  }
77459
77557
  async stop() {
77558
+ clearTimeout(this.startupTimer);
77460
77559
  if (!this.server) return;
77461
77560
  const server = this.server;
77462
77561
  this.server = null;
@@ -77488,7 +77587,9 @@ var MCPOAuthProvider = class {
77488
77587
  this.authServerMetadata = null;
77489
77588
  this.resourceMetadata = null;
77490
77589
  const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
77491
- const defaultScopes = getDefaultOAuthScopes();
77590
+ const defaultScopes = CONFIG.OAUTH_SCOPES ? getDefaultOAuthScopes() : [];
77591
+ this.explicitScopes = options.scopes ?? defaultScopes;
77592
+ this.configuredClientId = options.clientId || CONFIG.WP_OAUTH_CLIENT_ID || void 0;
77492
77593
  this.config = {
77493
77594
  serverUrl,
77494
77595
  resource: generateCanonicalResourceURI(serverUrl),
@@ -77547,187 +77648,217 @@ var MCPOAuthProvider = class {
77547
77648
  /**
77548
77649
  * Discover OAuth 2.1 server endpoints using MCP-compliant discovery
77549
77650
  */
77550
- async discoverOAuthEndpoints() {
77551
- try {
77552
- logger.oauth("Starting MCP-compliant OAuth endpoint discovery");
77553
- try {
77554
- this.resourceMetadata = await discoverProtectedResourceMetadata(this.config.resource);
77555
- logger.oauth("Protected resource metadata discovered");
77556
- } catch (error51) {
77557
- logger.warn(
77558
- "Protected resource metadata discovery failed, trying 401 response method",
77559
- "OAUTH"
77560
- );
77561
- await this.discoverVia401Response();
77562
- }
77563
- const authServerUrl = this.getAuthorizationServerUrl();
77564
- if (authServerUrl) {
77565
- this.authServerMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
77566
- logger.oauth("Authorization server metadata discovered");
77567
- this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
77568
- this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
77569
- this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
77570
- } else {
77571
- throw new OAuthError2("Could not determine authorization server URL", "DISCOVERY_FAILED");
77572
- }
77573
- } catch (error51) {
77574
- logger.error("OAuth endpoint discovery failed", "OAUTH", error51);
77575
- throw error51;
77576
- }
77577
- }
77578
- /**
77579
- * Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
77580
- */
77581
- async discoverVia401Response() {
77582
- try {
77583
- logger.oauth("Attempting discovery via 401 response method");
77584
- const customHeaders = getCustomHeaders();
77585
- const response = await proxyFetch(this.config.serverUrl, {
77586
- headers: {
77587
- Accept: "application/json",
77588
- ...customHeaders
77589
- }
77590
- });
77591
- if (response.status === 401) {
77592
- const wwwAuthHeader = response.headers.get("WWW-Authenticate");
77593
- if (wwwAuthHeader) {
77594
- const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
77595
- logger.oauth("WWW-Authenticate header parsed successfully");
77596
- if (authInfo.resource_metadata_url) {
77597
- const metadataResponse = await proxyFetch(authInfo.resource_metadata_url, {
77598
- headers: {
77599
- Accept: "application/json",
77600
- ...customHeaders
77601
- }
77602
- });
77603
- if (metadataResponse.ok) {
77604
- this.resourceMetadata = await metadataResponse.json();
77605
- logger.oauth("Protected resource metadata obtained via 401 response");
77606
- }
77607
- }
77608
- }
77609
- }
77610
- } catch (error51) {
77611
- logger.warn("401 response discovery method failed", "OAUTH", error51);
77612
- }
77651
+ async discoverOAuthEndpoints(challenge) {
77652
+ this.resourceMetadata = await discoverProtectedResourceMetadata(
77653
+ this.config.serverUrl,
77654
+ challenge?.resource_metadata
77655
+ );
77656
+ this.authServerMetadata = await discoverAuthorizationServerMetadata(
77657
+ this.resourceMetadata.authorization_servers[0]
77658
+ );
77659
+ this.config.resource = this.resourceMetadata.resource;
77660
+ this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
77661
+ this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
77662
+ this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
77663
+ this.config.clientId = this.configuredClientId;
77664
+ const challenged = challenge?.scope?.split(/\s+/).filter(Boolean);
77665
+ this.config.scopes = [
77666
+ ...new Set(
77667
+ challenged ?? (this.explicitScopes.length ? this.explicitScopes : this.resourceMetadata.scopes_supported ?? [])
77668
+ )
77669
+ ];
77613
77670
  }
77614
- /**
77615
- * Get authorization server URL from discovered metadata
77616
- */
77617
77671
  getAuthorizationServerUrl() {
77618
- if (this.resourceMetadata?.authorization_servers?.length) {
77619
- return this.resourceMetadata.authorization_servers[0];
77620
- }
77621
- try {
77622
- const url2 = new URL(this.config.serverUrl);
77623
- return url2.origin;
77624
- } catch {
77625
- return null;
77626
- }
77672
+ return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
77627
77673
  }
77628
77674
  /**
77629
77675
  * Perform dynamic client registration if needed and supported
77630
77676
  */
77631
77677
  async ensureClientRegistration() {
77678
+ const issuer = this.getAuthorizationServerUrl();
77679
+ const stored = await readClientInfo(this.serverUrlHash);
77632
77680
  if (this.config.clientId) {
77633
- logger.oauth("Using provided client ID, skipping dynamic registration");
77681
+ if (stored?.issuer && stored.issuer !== issuer && stored.client_id === this.config.clientId) {
77682
+ throw new OAuthError2(
77683
+ "Configured client belongs to another authorization server; update its registration",
77684
+ "CLIENT_ISSUER_MISMATCH"
77685
+ );
77686
+ }
77687
+ await writeClientInfo(this.serverUrlHash, { client_id: this.config.clientId, issuer });
77634
77688
  return;
77635
77689
  }
77636
- const storedClientInfo = await readClientInfo(this.serverUrlHash);
77637
- if (typeof storedClientInfo?.client_id === "string" && storedClientInfo.client_id.length > 0) {
77638
- this.config.clientId = storedClientInfo.client_id;
77639
- logger.oauth("Reusing persisted client registration, skipping dynamic registration");
77690
+ if (stored?.issuer === issuer && typeof stored.client_id === "string" && stored.client_id.length > 0) {
77691
+ this.config.clientId = stored.client_id;
77640
77692
  return;
77641
77693
  }
77642
- if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION) {
77694
+ if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
77643
77695
  throw new OAuthError2(
77644
- "No client ID provided and dynamic registration is disabled",
77696
+ "No issuer-bound client registration; configure a client ID or enable registration",
77645
77697
  "NO_CLIENT_ID"
77646
77698
  );
77647
- }
77648
- if (!this.authServerMetadata?.registration_endpoint) {
77699
+ if (!this.authServerMetadata?.registration_endpoint)
77649
77700
  throw new OAuthError2(
77650
- "Dynamic client registration not supported by authorization server",
77701
+ "Authorization server does not support dynamic registration; configure its client ID",
77651
77702
  "NO_DYNAMIC_REGISTRATION"
77652
77703
  );
77653
- }
77654
- try {
77655
- logger.oauth("Performing dynamic client registration");
77656
- const registrationRequest = {
77657
- redirect_uris: [this.config.redirectUri],
77658
- token_endpoint_auth_method: "none",
77659
- // Public client
77660
- grant_types: ["authorization_code"],
77661
- response_types: ["code"],
77662
- client_name: "WordPress MCP Remote",
77663
- scope: this.config.scopes.join(" ")
77664
- };
77665
- const registrationResponse = await registerDynamicClient(
77666
- this.authServerMetadata.registration_endpoint,
77667
- registrationRequest
77668
- );
77669
- const clientInfo = {
77670
- client_id: registrationResponse.client_id,
77671
- client_secret: registrationResponse.client_secret
77672
- };
77673
- await writeClientInfo(this.serverUrlHash, clientInfo);
77674
- this.config.clientId = registrationResponse.client_id;
77675
- logger.oauth("Dynamic client registration completed successfully");
77676
- } catch (error51) {
77677
- logger.error("Dynamic client registration failed", "OAUTH", error51);
77678
- throw error51;
77679
- }
77704
+ const request = {
77705
+ redirect_uris: [this.config.redirectUri],
77706
+ application_type: "native",
77707
+ token_endpoint_auth_method: "none",
77708
+ grant_types: ["authorization_code", "refresh_token"],
77709
+ response_types: ["code"],
77710
+ client_name: "WordPress MCP Remote",
77711
+ ...this.config.scopes.length ? { scope: this.config.scopes.join(" ") } : {}
77712
+ };
77713
+ const registered = await registerDynamicClient(
77714
+ this.authServerMetadata.registration_endpoint,
77715
+ request
77716
+ );
77717
+ await writeClientInfo(this.serverUrlHash, {
77718
+ ...request,
77719
+ client_id: registered.client_id,
77720
+ issuer
77721
+ });
77722
+ this.config.clientId = registered.client_id;
77680
77723
  }
77681
77724
  /**
77682
77725
  * Initiate MCP-compliant OAuth 2.1 authorization flow
77683
77726
  */
77684
- async authorize() {
77685
- if (this.authPromise) {
77686
- logger.oauth("Authorization already in progress, waiting...");
77687
- await this.authPromise;
77688
- return;
77689
- }
77690
- const existingTokens = await this.tokens();
77691
- if (existingTokens) {
77692
- logger.oauth("Already have valid tokens, skipping authorization");
77693
- return;
77727
+ satisfies(tokens, request) {
77728
+ return !!tokens && tokens.access_token !== request.rejectedToken && (!request.scopes?.length || request.scopes.every((scope) => tokens.scope?.split(" ").includes(scope)));
77729
+ }
77730
+ async authorize(request = {}) {
77731
+ if (this.authPromise) await this.authPromise;
77732
+ if (!this.authPromise) this.authPromise = this.authorizeWithLock(request);
77733
+ const pending = this.authPromise;
77734
+ try {
77735
+ await pending;
77736
+ } finally {
77737
+ if (this.authPromise === pending) this.authPromise = null;
77694
77738
  }
77695
- logger.oauth("Starting MCP-compliant OAuth 2.1 authorization flow");
77696
- this.authPromise = this.performAuthorization();
77739
+ }
77740
+ async authorizeWithLock(request) {
77741
+ const valid = await this.tokens();
77742
+ if (this.satisfies(valid, request)) return valid;
77743
+ await ensureConfigDir();
77744
+ const file2 = getConfigFilePath(this.serverUrlHash, "oauth");
77745
+ let release;
77697
77746
  try {
77698
- await this.authPromise;
77699
- logger.oauth("OAuth 2.1 authorization completed successfully");
77747
+ release = await lockfile.lock(file2, { realpath: false });
77700
77748
  } catch (error51) {
77701
- logger.error("OAuth 2.1 authorization failed", "OAUTH", error51);
77702
- throw error51;
77749
+ if (error51.code !== "ELOCKED") throw error51;
77750
+ const deadline = Date.now() + this.config.timeout;
77751
+ while (Date.now() < deadline) {
77752
+ const tokens = await this.tokens();
77753
+ if (this.satisfies(tokens, request)) return tokens;
77754
+ if (!await lockfile.check(file2, { realpath: false })) {
77755
+ const completed = await this.tokens();
77756
+ if (this.satisfies(completed, request)) return completed;
77757
+ throw new OAuthError2("Authentication in another process did not complete", "AUTH_FAILED");
77758
+ }
77759
+ await delay(Math.min(100, Math.max(0, deadline - Date.now())));
77760
+ }
77761
+ throw new OAuthError2("Timeout waiting for authentication in another process", "LOCK_TIMEOUT");
77762
+ }
77763
+ try {
77764
+ const completed = await this.tokens();
77765
+ if (this.satisfies(completed, request)) return completed;
77766
+ await this.discoverOAuthEndpoints(request.challenge);
77767
+ const issuer = this.getAuthorizationServerUrl();
77768
+ const stored = await readTokens(this.serverUrlHash);
77769
+ const client = await readClientInfo(this.serverUrlHash);
77770
+ if (this.configuredClientId && client?.issuer && client.issuer !== issuer && client.client_id === this.configuredClientId) {
77771
+ throw new OAuthError2(
77772
+ "Configured client belongs to another authorization server",
77773
+ "CLIENT_ISSUER_MISMATCH"
77774
+ );
77775
+ }
77776
+ this.config.scopes = [
77777
+ .../* @__PURE__ */ new Set([
77778
+ ...stored?.issuer === issuer ? stored.scope?.split(" ").filter(Boolean) ?? [] : [],
77779
+ ...this.config.scopes,
77780
+ ...request.scopes ?? []
77781
+ ])
77782
+ ];
77783
+ const needsScope = !!request.scopes?.some(
77784
+ (scope) => !stored?.scope?.split(" ").includes(scope)
77785
+ );
77786
+ const clientId = this.configuredClientId || client?.client_id;
77787
+ if (stored?.issuer === issuer && client?.issuer === issuer && stored.refresh_token && clientId && (!this.configuredClientId || this.configuredClientId === client.client_id) && !needsScope) {
77788
+ try {
77789
+ const renewed = await refreshAuthorization(issuer, {
77790
+ metadata: { ...this.authServerMetadata },
77791
+ clientInformation: { client_id: clientId },
77792
+ refreshToken: stored.refresh_token,
77793
+ resource: this.resourceIndicator(),
77794
+ fetchFn: (url2, init) => oauthFetch(String(url2), init)
77795
+ });
77796
+ if (renewed.token_type.toLowerCase() !== "bearer")
77797
+ throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
77798
+ const tokens = {
77799
+ ...renewed,
77800
+ issuer,
77801
+ scope: renewed.scope ?? stored.scope,
77802
+ obtained_at: Date.now()
77803
+ };
77804
+ await writeTokens(this.serverUrlHash, tokens);
77805
+ return tokens;
77806
+ } catch (error51) {
77807
+ if (!(error51 instanceof InvalidGrantError)) throw error51;
77808
+ await deleteConfigFile(this.serverUrlHash, "tokens.json");
77809
+ }
77810
+ }
77811
+ return await this.performAuthorization();
77703
77812
  } finally {
77704
- this.authPromise = null;
77813
+ await release();
77705
77814
  }
77706
77815
  }
77816
+ resourceIndicator() {
77817
+ if (!CONFIG.OAUTH_RESOURCE_INDICATOR)
77818
+ throw new OAuthError2(
77819
+ "MCP OAuth requires resource indicators; remove OAUTH_RESOURCE_INDICATOR=false",
77820
+ "RESOURCE_REQUIRED"
77821
+ );
77822
+ return this.config.resource;
77823
+ }
77707
77824
  /**
77708
77825
  * Perform the complete OAuth 2.1 authorization flow
77709
77826
  */
77710
77827
  async performAuthorization() {
77711
77828
  try {
77712
- await this.discoverOAuthEndpoints();
77713
- const callbackPort = this.config.callbackPort === 0 ? await getOAuthCallbackPort() : this.config.callbackPort;
77829
+ if (!this.authServerMetadata) await this.discoverOAuthEndpoints();
77830
+ if (!this.authServerMetadata?.code_challenge_methods_supported?.includes("S256"))
77831
+ throw new OAuthError2(
77832
+ "Authorization server must advertise PKCE S256 support",
77833
+ "PKCE_REQUIRED"
77834
+ );
77835
+ this.resourceIndicator();
77836
+ if (!["127.0.0.1", "localhost", "::1"].includes(this.config.host))
77837
+ throw new OAuthError2(
77838
+ "Native OAuth callback must bind to loopback",
77839
+ "INVALID_CALLBACK_HOST"
77840
+ );
77841
+ this.currentPKCE = generatePKCE();
77842
+ this.currentState = generateSecureState();
77843
+ const callbackPort = this.config.callbackPort === 0 ? await selectCallbackPort(this.config.serverUrl, void 0, false, this.config.host) : this.config.callbackPort;
77714
77844
  const callbackServer = setupWPOAuthCallbackServer(
77715
77845
  {
77716
77846
  port: callbackPort,
77717
77847
  host: this.config.host,
77718
77848
  serverUrlHash: this.serverUrlHash,
77719
- timeout: this.config.timeout
77849
+ timeout: this.config.timeout,
77850
+ state: this.currentState,
77851
+ issuer: this.authServerMetadata.issuer,
77852
+ requireIssuer: this.authServerMetadata.authorization_response_iss_parameter_supported
77720
77853
  },
77721
77854
  this.events
77722
77855
  );
77723
77856
  await callbackServer.start();
77724
77857
  logger.oauth("OAuth callback server started");
77725
77858
  try {
77726
- const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
77859
+ const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
77727
77860
  this.config.redirectUri = actualRedirectUri;
77728
77861
  await this.ensureClientRegistration();
77729
- this.currentPKCE = generatePKCE();
77730
- this.currentState = generateSecureState();
77731
77862
  await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
77732
77863
  await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
77733
77864
  const authUrl = buildAuthorizationUrl(
@@ -77737,10 +77868,13 @@ var MCPOAuthProvider = class {
77737
77868
  this.config.scopes,
77738
77869
  this.currentState,
77739
77870
  this.currentPKCE.codeChallenge,
77740
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77871
+ this.resourceIndicator()
77741
77872
  );
77742
77873
  logger.oauth("Built OAuth 2.1 authorization URL");
77743
77874
  logger.debug("Authorization URL", "OAUTH", { url: authUrl });
77875
+ const authorizationCode = this.waitForAuthorizationCode();
77876
+ void authorizationCode.catch(() => {
77877
+ });
77744
77878
  try {
77745
77879
  await open_default(authUrl);
77746
77880
  logger.oauth("Browser opened successfully");
@@ -77751,13 +77885,20 @@ var MCPOAuthProvider = class {
77751
77885
  logger.info(`${authUrl}`);
77752
77886
  logger.info("===============================\n");
77753
77887
  }
77754
- const authCode = await this.waitForAuthorizationCode();
77755
- const tokens = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
77888
+ const authCode = await authorizationCode;
77889
+ const response = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
77890
+ const tokens = {
77891
+ ...response,
77892
+ issuer: this.getAuthorizationServerUrl(),
77893
+ scope: response.scope ?? this.config.scopes.join(" ")
77894
+ };
77756
77895
  await writeTokens(this.serverUrlHash, tokens);
77757
77896
  logger.oauth("OAuth 2.1 tokens stored successfully");
77758
77897
  return tokens;
77759
77898
  } finally {
77760
77899
  await callbackServer.stop();
77900
+ await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77901
+ await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
77761
77902
  }
77762
77903
  } catch (error51) {
77763
77904
  logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
@@ -77778,12 +77919,16 @@ var MCPOAuthProvider = class {
77778
77919
  this.events.removeAllListeners("oauth-error");
77779
77920
  clearTimeout(timeout);
77780
77921
  };
77781
- this.events.once("oauth-code-received", async ({ code, state }) => {
77922
+ this.events.once("oauth-code-received", async ({ code, state, iss }) => {
77782
77923
  cleanup();
77783
77924
  if (state !== this.currentState) {
77784
77925
  reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
77785
77926
  return;
77786
77927
  }
77928
+ if ((iss !== void 0 || this.authServerMetadata?.authorization_response_iss_parameter_supported) && iss !== this.authServerMetadata?.issuer) {
77929
+ reject(new OAuthError2("Authorization response issuer mismatch", "INVALID_ISSUER"));
77930
+ return;
77931
+ }
77787
77932
  logger.oauth("Valid authorization code received");
77788
77933
  resolve(code);
77789
77934
  });
@@ -77809,7 +77954,7 @@ var MCPOAuthProvider = class {
77809
77954
  redirectUri,
77810
77955
  this.config.clientId,
77811
77956
  codeVerifier,
77812
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77957
+ this.resourceIndicator()
77813
77958
  );
77814
77959
  await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77815
77960
  await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
@@ -77875,6 +78020,7 @@ import * as path5 from "node:path";
77875
78020
  import * as os3 from "node:os";
77876
78021
 
77877
78022
  // src/lib/persistent-oauth-client-provider.ts
78023
+ import { randomBytes } from "node:crypto";
77878
78024
  import { EventEmitter as EventEmitter2 } from "node:events";
77879
78025
  var PersistentWPOAuthClientProvider = class {
77880
78026
  constructor(options) {
@@ -78032,7 +78178,10 @@ var PersistentWPOAuthClientProvider = class {
78032
78178
  */
78033
78179
  async performAuthorization() {
78034
78180
  const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
78181
+ const state = this.generateState();
78035
78182
  const callbackServerOptions = {
78183
+ flow: "implicit",
78184
+ state,
78036
78185
  port: callbackPort,
78037
78186
  host: this.options.host,
78038
78187
  serverUrlHash: this.serverUrlHash,
@@ -78044,12 +78193,14 @@ var PersistentWPOAuthClientProvider = class {
78044
78193
  logger.oauth("Starting callback server...");
78045
78194
  await callbackServer.start();
78046
78195
  logger.oauth("Callback server started successfully");
78047
- const state = this.generateState();
78048
78196
  logger.debug(`Generated state parameter: ${state}`, "OAUTH");
78049
78197
  const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
78050
78198
  logger.oauth(`Built authorization URL: ${authUrl}`);
78051
78199
  logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
78052
78200
  logger.oauth("Attempting to open browser...");
78201
+ const authorization = this.waitForAuthorizationResult();
78202
+ void authorization.catch(() => {
78203
+ });
78053
78204
  try {
78054
78205
  await open_default(authUrl);
78055
78206
  logger.oauth("Browser opened successfully");
@@ -78061,7 +78212,7 @@ var PersistentWPOAuthClientProvider = class {
78061
78212
  logger.info("===============================\n");
78062
78213
  }
78063
78214
  logger.oauth("Waiting for authorization result...");
78064
- const tokens = await this.waitForAuthorizationResult();
78215
+ const tokens = await authorization;
78065
78216
  logger.oauth("Authorization result received");
78066
78217
  logger.oauth("Saving tokens to persistent storage...");
78067
78218
  await this.saveTokens(tokens);
@@ -78114,7 +78265,7 @@ var PersistentWPOAuthClientProvider = class {
78114
78265
  * Generate a secure state parameter
78115
78266
  */
78116
78267
  generateState() {
78117
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
78268
+ return randomBytes(32).toString("base64url");
78118
78269
  }
78119
78270
  /**
78120
78271
  * Wait for authorization result from callback server
@@ -78537,6 +78688,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
78537
78688
  return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
78538
78689
  }
78539
78690
  }
78691
+ function getMCPOAuthProvider() {
78692
+ return mcpOAuthProvider ??= new MCPOAuthProvider({
78693
+ serverUrl: getRequestUrl(),
78694
+ clientId: CONFIG.WP_OAUTH_CLIENT_ID
78695
+ });
78696
+ }
78540
78697
  async function getOAuthTokens() {
78541
78698
  try {
78542
78699
  if (!CONFIG.OAUTH_ENABLED) {
@@ -78544,7 +78701,7 @@ async function getOAuthTokens() {
78544
78701
  return null;
78545
78702
  }
78546
78703
  logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
78547
- const serverUrl = CONFIG.WP_API_URL;
78704
+ const serverUrl = getRequestUrl();
78548
78705
  const serverUrlHash = generateServerUrlHash(serverUrl);
78549
78706
  const existingTokens = await getValidTokens(serverUrlHash);
78550
78707
  if (existingTokens) {
@@ -78555,16 +78712,10 @@ async function getOAuthTokens() {
78555
78712
  logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
78556
78713
  logger.auth("Your browser should open automatically for authentication");
78557
78714
  if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE) {
78558
- if (!mcpOAuthProvider) {
78559
- mcpOAuthProvider = new MCPOAuthProvider({
78560
- serverUrl,
78561
- clientId: CONFIG.WP_OAUTH_CLIENT_ID,
78562
- scopes: getDefaultOAuthScopes()
78563
- });
78564
- }
78565
78715
  logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
78566
- await mcpOAuthProvider.authorize();
78567
- const tokens = await mcpOAuthProvider.tokens();
78716
+ const provider = getMCPOAuthProvider();
78717
+ await provider.authorize();
78718
+ const tokens = await provider.tokens();
78568
78719
  if (tokens) {
78569
78720
  logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
78570
78721
  return tokens;
@@ -78661,7 +78812,7 @@ function updateSessionId(sessionId) {
78661
78812
  globalSessionId = sessionId;
78662
78813
  logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
78663
78814
  }
78664
- async function getWordPressHeaders(requestData) {
78815
+ async function getWordPressHeaders(requestData, options = {}) {
78665
78816
  validateEnvironment();
78666
78817
  let authHeader = "";
78667
78818
  if (CONFIG.JWT_TOKEN) {
@@ -78670,7 +78821,7 @@ async function getWordPressHeaders(requestData) {
78670
78821
  logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
78671
78822
  } else if (CONFIG.OAUTH_ENABLED) {
78672
78823
  logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
78673
- const oauthTokens = await getOAuthTokens();
78824
+ const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
78674
78825
  if (oauthTokens) {
78675
78826
  authHeader = `Bearer ${oauthTokens.access_token}`;
78676
78827
  logger.auth("Using OAuth token authentication for WordPress API");
@@ -78710,7 +78861,7 @@ async function getWordPressHeaders(requestData) {
78710
78861
  if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
78711
78862
  }
78712
78863
  const hasCustomHeaders = Object.keys(customHeaders).length > 0;
78713
- if (!authHeader && !hasCustomHeaders) {
78864
+ if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
78714
78865
  throw new AuthError(
78715
78866
  "No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
78716
78867
  "NO_AUTH_METHOD"