@automattic/mcp-wordpress-remote 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib.js CHANGED
@@ -6498,7 +6498,7 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
6498
6498
  const LAST_CHUNK = Buffer3.from("0\r\n\r\n");
6499
6499
  let isChunkedTransfer = false;
6500
6500
  let properLastChunkReceived = false;
6501
- let previousChunk;
6501
+ let trailingBytes = Buffer3.alloc(0);
6502
6502
  request.on("response", (response) => {
6503
6503
  const { headers } = response;
6504
6504
  isChunkedTransfer = headers["transfer-encoding"] === "chunked" && !headers["content-length"];
@@ -6512,11 +6512,15 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
6512
6512
  }
6513
6513
  };
6514
6514
  const onData = (buf) => {
6515
- properLastChunkReceived = Buffer3.compare(buf.slice(-5), LAST_CHUNK) === 0;
6516
- if (!properLastChunkReceived && previousChunk) {
6517
- properLastChunkReceived = Buffer3.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 && Buffer3.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0;
6515
+ if (buf.length === 0) return;
6516
+ if (buf.length >= LAST_CHUNK.length) {
6517
+ trailingBytes = Buffer3.from(buf.subarray(-LAST_CHUNK.length));
6518
+ } else {
6519
+ trailingBytes = Buffer3.from(
6520
+ Buffer3.concat([trailingBytes, buf]).subarray(-LAST_CHUNK.length)
6521
+ );
6518
6522
  }
6519
- previousChunk = buf;
6523
+ properLastChunkReceived = trailingBytes.equals(LAST_CHUNK);
6520
6524
  };
6521
6525
  socket.prependListener("close", onSocketClose);
6522
6526
  socket.on("data", onData);
@@ -75554,8 +75558,152 @@ var OAUTH_ERRORS = {
75554
75558
  };
75555
75559
 
75556
75560
  // node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js
75561
+ function isClientAuthMethod(method) {
75562
+ return ["client_secret_basic", "client_secret_post", "none"].includes(method);
75563
+ }
75564
+ function selectClientAuthMethod(clientInformation, supportedMethods) {
75565
+ const hasClientSecret = clientInformation.client_secret !== void 0;
75566
+ 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))) {
75567
+ return clientInformation.token_endpoint_auth_method;
75568
+ }
75569
+ if (supportedMethods.length === 0) {
75570
+ return hasClientSecret ? "client_secret_basic" : "none";
75571
+ }
75572
+ if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
75573
+ return "client_secret_basic";
75574
+ }
75575
+ if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
75576
+ return "client_secret_post";
75577
+ }
75578
+ if (supportedMethods.includes("none")) {
75579
+ return "none";
75580
+ }
75581
+ return hasClientSecret ? "client_secret_post" : "none";
75582
+ }
75583
+ function applyClientAuthentication(method, clientInformation, headers, params) {
75584
+ const { client_id, client_secret } = clientInformation;
75585
+ switch (method) {
75586
+ case "client_secret_basic":
75587
+ applyBasicAuth(client_id, client_secret, headers);
75588
+ return;
75589
+ case "client_secret_post":
75590
+ applyPostAuth(client_id, client_secret, params);
75591
+ return;
75592
+ case "none":
75593
+ applyPublicAuth(client_id, params);
75594
+ return;
75595
+ default:
75596
+ throw new Error(`Unsupported client authentication method: ${method}`);
75597
+ }
75598
+ }
75599
+ function applyBasicAuth(clientId, clientSecret, headers) {
75600
+ if (!clientSecret) {
75601
+ throw new Error("client_secret_basic authentication requires a client_secret");
75602
+ }
75603
+ const credentials = btoa(`${clientId}:${clientSecret}`);
75604
+ headers.set("Authorization", `Basic ${credentials}`);
75605
+ }
75606
+ function applyPostAuth(clientId, clientSecret, params) {
75607
+ params.set("client_id", clientId);
75608
+ if (clientSecret) {
75609
+ params.set("client_secret", clientSecret);
75610
+ }
75611
+ }
75612
+ function applyPublicAuth(clientId, params) {
75613
+ params.set("client_id", clientId);
75614
+ }
75615
+ async function parseErrorResponse(input) {
75616
+ const statusCode = input instanceof Response ? input.status : void 0;
75617
+ const body = input instanceof Response ? await input.text() : input;
75618
+ try {
75619
+ const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
75620
+ const { error: error51, error_description, error_uri } = result;
75621
+ const errorClass = OAUTH_ERRORS[error51] || ServerError;
75622
+ return new errorClass(error_description || "", error_uri);
75623
+ } catch (error51) {
75624
+ const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error51}. Raw body: ${body}`;
75625
+ return new ServerError(errorMessage);
75626
+ }
75627
+ }
75557
75628
  var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
75558
75629
  var RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true });
75630
+ function buildDiscoveryUrls(authorizationServerUrl) {
75631
+ const url2 = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
75632
+ const hasPath = url2.pathname !== "/";
75633
+ const urlsToTry = [];
75634
+ if (!hasPath) {
75635
+ urlsToTry.push({
75636
+ url: new URL("/.well-known/oauth-authorization-server", url2.origin),
75637
+ type: "oauth"
75638
+ });
75639
+ urlsToTry.push({
75640
+ url: new URL(`/.well-known/openid-configuration`, url2.origin),
75641
+ type: "oidc"
75642
+ });
75643
+ return urlsToTry;
75644
+ }
75645
+ let pathname = url2.pathname;
75646
+ if (pathname.endsWith("/")) {
75647
+ pathname = pathname.slice(0, -1);
75648
+ }
75649
+ urlsToTry.push({
75650
+ url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url2.origin),
75651
+ type: "oauth"
75652
+ });
75653
+ urlsToTry.push({
75654
+ url: new URL(`/.well-known/openid-configuration${pathname}`, url2.origin),
75655
+ type: "oidc"
75656
+ });
75657
+ urlsToTry.push({
75658
+ url: new URL(`${pathname}/.well-known/openid-configuration`, url2.origin),
75659
+ type: "oidc"
75660
+ });
75661
+ return urlsToTry;
75662
+ }
75663
+ function resourceIndicatorToString(resource) {
75664
+ return typeof resource === "string" ? resource : resource.href;
75665
+ }
75666
+ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
75667
+ const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
75668
+ const headers = new Headers({
75669
+ "Content-Type": "application/x-www-form-urlencoded",
75670
+ Accept: "application/json"
75671
+ });
75672
+ if (resource) {
75673
+ tokenRequestParams.set("resource", resourceIndicatorToString(resource));
75674
+ }
75675
+ if (addClientAuthentication) {
75676
+ await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
75677
+ } else if (clientInformation) {
75678
+ const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
75679
+ const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
75680
+ applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
75681
+ }
75682
+ const response = await (fetchFn ?? fetch)(tokenUrl, {
75683
+ method: "POST",
75684
+ headers,
75685
+ body: tokenRequestParams
75686
+ });
75687
+ if (!response.ok) {
75688
+ throw await parseErrorResponse(response);
75689
+ }
75690
+ return TokenResponseSchema.parse(await response.json());
75691
+ }
75692
+ async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
75693
+ const tokenRequestParams = new URLSearchParams({
75694
+ grant_type: "refresh_token",
75695
+ refresh_token: refreshToken
75696
+ });
75697
+ const tokens = await executeTokenRequest(authorizationServerUrl, {
75698
+ metadata,
75699
+ tokenRequestParams,
75700
+ clientInformation,
75701
+ addClientAuthentication,
75702
+ resource,
75703
+ fetchFn
75704
+ });
75705
+ return { refresh_token: refreshToken, ...tokens };
75706
+ }
75559
75707
 
75560
75708
  // src/lib/utils.ts
75561
75709
  import * as fs2 from "node:fs";
@@ -75668,7 +75816,19 @@ async function writeTokens(serverUrlHash, tokens) {
75668
75816
  ...tokens,
75669
75817
  obtained_at: Date.now()
75670
75818
  };
75671
- await writeJsonFile(serverUrlHash, "tokens.json", tokensWithTimestamp);
75819
+ await ensureConfigDir();
75820
+ const file2 = getConfigFilePath(serverUrlHash, "tokens.json");
75821
+ const temporary = `${file2}.${crypto2.randomUUID()}.tmp`;
75822
+ try {
75823
+ await fs.writeFile(temporary, JSON.stringify(tokensWithTimestamp, null, 2), {
75824
+ encoding: "utf-8",
75825
+ mode: 384,
75826
+ flag: "wx"
75827
+ });
75828
+ await fs.rename(temporary, file2);
75829
+ } finally {
75830
+ await fs.rm(temporary, { force: true });
75831
+ }
75672
75832
  logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
75673
75833
  } catch (error51) {
75674
75834
  logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
@@ -75701,7 +75861,7 @@ function isTokenValid(tokens) {
75701
75861
  if (!tokens?.access_token) {
75702
75862
  return { isValid: false, error: "No access token" };
75703
75863
  }
75704
- if (!tokens.expires_in || !tokens.obtained_at) {
75864
+ if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
75705
75865
  return { isValid: true };
75706
75866
  }
75707
75867
  const now = Date.now();
@@ -75740,12 +75900,12 @@ var OAuthClientInfoSchema = external_exports.object({
75740
75900
  client_id: external_exports.string().optional(),
75741
75901
  client_secret: external_exports.string().optional()
75742
75902
  });
75743
- async function findAvailablePort(preferredPort) {
75903
+ async function findAvailablePort(preferredPort, host = "127.0.0.1") {
75744
75904
  return new Promise((resolve, reject) => {
75745
75905
  const server = net.createServer();
75746
75906
  server.on("error", (err) => {
75747
75907
  if (err.code === "EADDRINUSE") {
75748
- server.listen(0);
75908
+ server.listen(0, host);
75749
75909
  } else {
75750
75910
  reject(err);
75751
75911
  }
@@ -75756,7 +75916,7 @@ async function findAvailablePort(preferredPort) {
75756
75916
  resolve(port);
75757
75917
  });
75758
75918
  });
75759
- server.listen(preferredPort || 0);
75919
+ server.listen(preferredPort || 0, host);
75760
75920
  });
75761
75921
  }
75762
75922
  function calculateDefaultPort(serverUrlHash) {
@@ -75784,24 +75944,17 @@ async function findExistingClientPort(serverUrlHash) {
75784
75944
  function getServerUrlHash(serverUrl) {
75785
75945
  return crypto3.createHash("md5").update(serverUrl).digest("hex");
75786
75946
  }
75787
- async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
75947
+ async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
75788
75948
  const serverUrlHash = getServerUrlHash(serverUrl);
75789
75949
  if (specifiedPort) {
75790
75950
  return specifiedPort;
75791
75951
  }
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;
75952
+ const existingClientPort = await findExistingClientPort(serverUrlHash);
75953
+ return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
75801
75954
  }
75802
75955
 
75803
75956
  // src/lib/config.ts
75804
- var MCP_WORDPRESS_REMOTE_VERSION = "0.5.2";
75957
+ var MCP_WORDPRESS_REMOTE_VERSION = "0.5.4";
75805
75958
  function parsePositiveIntEnv(value, defaultValue) {
75806
75959
  if (!value) {
75807
75960
  return defaultValue;
@@ -76002,8 +76155,8 @@ async function getOAuthCallbackPort() {
76002
76155
  return selectCallbackPort(
76003
76156
  CONFIG.WP_API_URL,
76004
76157
  CONFIG.OAUTH_CALLBACK_PORT,
76005
- false
76006
- // Always treat as self-hosted for port selection
76158
+ false,
76159
+ CONFIG.OAUTH_HOST
76007
76160
  );
76008
76161
  }
76009
76162
  function formatConfigError(error51) {
@@ -76923,10 +77076,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
76923
77076
  var open_default = open;
76924
77077
 
76925
77078
  // src/lib/mcp-oauth-provider.ts
77079
+ import lockfile from "proper-lockfile";
77080
+ import { setTimeout as delay } from "node:timers/promises";
76926
77081
  import { EventEmitter } from "node:events";
76927
77082
 
76928
77083
  // src/lib/mcp-oauth-utils.ts
76929
77084
  import crypto4 from "crypto";
77085
+
77086
+ // src/lib/oauth-http.ts
77087
+ function validateOAuthUrl(value) {
77088
+ let url2;
77089
+ try {
77090
+ url2 = new URL(value);
77091
+ } catch {
77092
+ throw new OAuthError2("Invalid OAuth URL", "INVALID_OAUTH_URL");
77093
+ }
77094
+ const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
77095
+ if (url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback) || url2.username || url2.password || url2.hash) {
77096
+ throw new OAuthError2(
77097
+ "OAuth URLs require HTTPS (HTTP is allowed only on loopback)",
77098
+ "INSECURE_OAUTH_URL"
77099
+ );
77100
+ }
77101
+ return url2;
77102
+ }
77103
+ async function oauthFetch(value, init = {}) {
77104
+ const url2 = validateOAuthUrl(value);
77105
+ const headers = new Headers(
77106
+ url2.origin === new URL(CONFIG.WP_API_URL).origin ? getCustomHeaders() : {}
77107
+ );
77108
+ new Headers(init.headers).forEach((value2, key) => headers.set(key, value2));
77109
+ const response = await proxyFetch(url2.toString(), {
77110
+ ...init,
77111
+ headers,
77112
+ redirect: "error",
77113
+ signal: init.signal ?? AbortSignal.timeout(CONFIG.OAUTH_TIMEOUT)
77114
+ });
77115
+ if (response.status >= 300 && response.status < 400)
77116
+ throw new OAuthError2("OAuth endpoint redirects are not accepted", "OAUTH_REDIRECT");
77117
+ return response;
77118
+ }
77119
+
77120
+ // src/lib/mcp-oauth-utils.ts
76930
77121
  function generatePKCE() {
76931
77122
  const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
76932
77123
  const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
@@ -76954,118 +77145,76 @@ function generateCanonicalResourceURI(serverUrl) {
76954
77145
  }
76955
77146
  }
76956
77147
  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) {
77148
+ const issuer = validateOAuthUrl(authorizationServerUrl);
77149
+ if (issuer.search) throw new OAuthError2("Issuer must not contain a query", "INVALID_METADATA");
77150
+ for (const { url: url2 } of buildDiscoveryUrls(issuer)) {
77151
+ const response = await oauthFetch(url2.toString(), { headers: { Accept: "application/json" } });
77152
+ if ([404, 405].includes(response.status)) continue;
77153
+ if (!response.ok)
76968
77154
  throw new OAuthError2(
76969
- `Failed to fetch authorization server metadata: ${response.status}`,
77155
+ `Metadata request failed (${response.status})`,
76970
77156
  "METADATA_DISCOVERY_FAILED"
76971
77157
  );
76972
- }
76973
- const metadata = await response.json();
76974
- if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
77158
+ const parsed = OAuthMetadataSchema.safeParse(await response.json());
77159
+ if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
76975
77160
  throw new OAuthError2(
76976
- "Authorization server metadata missing required endpoints",
77161
+ "Authorization metadata has an invalid or mismatched issuer",
76977
77162
  "INVALID_METADATA"
76978
77163
  );
76979
77164
  }
76980
- logger.oauth("Authorization server metadata discovered successfully");
77165
+ const metadata = parsed.data;
77166
+ validateOAuthUrl(metadata.issuer);
77167
+ validateOAuthUrl(metadata.authorization_endpoint);
77168
+ validateOAuthUrl(metadata.token_endpoint);
77169
+ if (metadata.registration_endpoint) validateOAuthUrl(metadata.registration_endpoint);
76981
77170
  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
77171
  }
77172
+ throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
76991
77173
  }
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) {
77174
+ async function discoverProtectedResourceMetadata(resourceUrl, resourceMetadataUrl) {
77175
+ const resource = validateOAuthUrl(resourceUrl);
77176
+ const candidates = resourceMetadataUrl ? [{ url: resourceMetadataUrl, expected: resourceUrl }] : [
77177
+ ...resource.pathname !== "/" ? [
77178
+ {
77179
+ url: `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`,
77180
+ expected: resourceUrl
77181
+ }
77182
+ ] : [],
77183
+ {
77184
+ url: `${resource.origin}/.well-known/oauth-protected-resource`,
77185
+ expected: resource.pathname === "/" ? resourceUrl : resource.origin
77186
+ }
77187
+ ];
77188
+ for (const candidate of candidates) {
77189
+ const response = await oauthFetch(candidate.url, { headers: { Accept: "application/json" } });
77190
+ if (!resourceMetadataUrl && [404, 405].includes(response.status)) continue;
77191
+ if (!response.ok)
77005
77192
  throw new OAuthError2(
77006
- `Failed to fetch protected resource metadata: ${response.status}`,
77193
+ `Resource metadata request failed (${response.status})`,
77007
77194
  "RESOURCE_METADATA_DISCOVERY_FAILED"
77008
77195
  );
77009
- }
77010
- const metadata = await response.json();
77011
- if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
77196
+ const parsed = OAuthProtectedResourceMetadataSchema.safeParse(await response.json());
77197
+ if (!parsed.success || parsed.data.resource !== candidate.expected || !parsed.data.authorization_servers?.length) {
77012
77198
  throw new OAuthError2(
77013
- "Protected resource metadata missing authorization servers",
77199
+ "Protected resource metadata has an invalid or mismatched resource",
77014
77200
  "INVALID_RESOURCE_METADATA"
77015
77201
  );
77016
77202
  }
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
- );
77203
+ for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
77204
+ return parsed.data;
77027
77205
  }
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;
77206
+ throw new OAuthError2(
77207
+ "No protected resource metadata found",
77208
+ "RESOURCE_METADATA_DISCOVERY_FAILED"
77209
+ );
77059
77210
  }
77060
77211
  async function registerDynamicClient(registrationEndpoint, registrationRequest) {
77061
77212
  try {
77062
77213
  logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
77063
- const customHeaders = getCustomHeaders();
77064
- const response = await proxyFetch(registrationEndpoint, {
77214
+ const response = await oauthFetch(registrationEndpoint, {
77065
77215
  method: "POST",
77066
77216
  headers: {
77067
- "Content-Type": "application/json",
77068
- ...customHeaders
77217
+ "Content-Type": "application/json"
77069
77218
  },
77070
77219
  body: JSON.stringify(registrationRequest)
77071
77220
  });
@@ -77077,12 +77226,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
77077
77226
  );
77078
77227
  }
77079
77228
  const registrationResponse = await response.json();
77080
- if (!registrationResponse.client_id) {
77229
+ if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
77081
77230
  throw new OAuthError2(
77082
77231
  "Dynamic client registration response missing client_id",
77083
77232
  "INVALID_REGISTRATION_RESPONSE"
77084
77233
  );
77085
77234
  }
77235
+ if (registrationResponse.token_endpoint_auth_method && registrationResponse.token_endpoint_auth_method !== registrationRequest.token_endpoint_auth_method) {
77236
+ throw new OAuthError2(
77237
+ "Authorization server changed the requested client authentication method",
77238
+ "INVALID_REGISTRATION_RESPONSE"
77239
+ );
77240
+ }
77241
+ if (registrationResponse.redirect_uris && (!Array.isArray(registrationResponse.redirect_uris) || !registrationRequest.redirect_uris.every(
77242
+ (uri) => registrationResponse.redirect_uris.includes(uri)
77243
+ ))) {
77244
+ throw new OAuthError2(
77245
+ "Authorization server changed the registered callback URI",
77246
+ "INVALID_REGISTRATION_RESPONSE"
77247
+ );
77248
+ }
77086
77249
  logger.oauth("Dynamic client registration successful");
77087
77250
  logger.debug("Client ID obtained", "OAUTH", {
77088
77251
  client_id: registrationResponse.client_id,
@@ -77112,12 +77275,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77112
77275
  if (resource) {
77113
77276
  params.set("resource", resource);
77114
77277
  }
77115
- const customHeaders = getCustomHeaders();
77116
- const response = await proxyFetch(tokenEndpoint, {
77278
+ const response = await oauthFetch(tokenEndpoint, {
77117
77279
  method: "POST",
77118
77280
  headers: {
77119
- "Content-Type": "application/x-www-form-urlencoded",
77120
- ...customHeaders
77281
+ "Content-Type": "application/x-www-form-urlencoded"
77121
77282
  },
77122
77283
  body: params.toString()
77123
77284
  });
@@ -77128,7 +77289,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77128
77289
  "TOKEN_EXCHANGE_FAILED"
77129
77290
  );
77130
77291
  }
77131
- const tokenResponse = await response.json();
77292
+ const tokenResponse = OAuthTokensSchema.parse(await response.json());
77293
+ if (tokenResponse.token_type.toLowerCase() !== "bearer")
77294
+ throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
77132
77295
  if (!tokenResponse.access_token) {
77133
77296
  throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
77134
77297
  }
@@ -77157,15 +77320,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
77157
77320
  // OAuth 2.1 uses authorization code flow
77158
77321
  client_id: clientId,
77159
77322
  redirect_uri: redirectUri,
77160
- scope: scopes.join(" "),
77161
77323
  state,
77162
77324
  code_challenge: codeChallenge,
77163
77325
  code_challenge_method: "S256"
77164
77326
  });
77327
+ if (scopes.length) params.set("scope", scopes.join(" "));
77165
77328
  if (resource) {
77166
77329
  params.set("resource", resource);
77167
77330
  }
77168
- return `${authorizationEndpoint}?${params.toString()}`;
77331
+ validateOAuthUrl(authorizationEndpoint);
77332
+ const url2 = new URL(authorizationEndpoint);
77333
+ params.forEach((value, key) => url2.searchParams.set(key, value));
77334
+ return url2.toString();
77169
77335
  }
77170
77336
  function generateSecureState() {
77171
77337
  return crypto4.randomBytes(32).toString("base64url");
@@ -77246,110 +77412,26 @@ var AUTHORIZATION_CODE_HTML = `
77246
77412
  </div>
77247
77413
 
77248
77414
  <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.');
77415
+ const query = new URLSearchParams(window.location.search);
77416
+ const fragment = new URLSearchParams(window.location.hash.substring(1));
77417
+ const status = document.getElementById('status');
77418
+ const details = document.getElementById('details');
77419
+ const data = Object.fromEntries(query.has('code') || query.has('error') ? query : fragment);
77420
+ const endpoint = data.access_token ? '/oauth/tokens' : '/oauth/callback';
77421
+ if (data.code || data.error || data.access_token) {
77422
+ fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
77423
+ .then(async response => {
77424
+ const result = await response.json();
77425
+ if (!response.ok) throw new Error(result.error || 'Authorization response was rejected');
77426
+ status.textContent = data.access_token ? 'Authorization successful' : 'Authorization code received';
77427
+ status.className = 'success';
77428
+ details.textContent = data.access_token ? 'You can close this window.' : 'Return to your app to check the connection. Token exchange may still fail.';
77429
+ })
77430
+ .catch(error => { status.textContent = 'Authorization failed'; status.className = 'error'; details.textContent = error.message; })
77431
+ .finally(() => { document.getElementById('spinner').style.display = 'none'; });
77432
+ } else {
77433
+ status.textContent = 'No authorization response received';
77434
+ document.getElementById('spinner').style.display = 'none';
77353
77435
  }
77354
77436
  </script>
77355
77437
  </body>
@@ -77364,6 +77446,17 @@ var OAuthCallbackServer = class {
77364
77446
  this.setupRoutes();
77365
77447
  }
77366
77448
  setupRoutes() {
77449
+ this.app.use((req, res, next) => {
77450
+ const host = this.options.host === "::1" ? "[::1]" : this.options.host;
77451
+ const origin = req.headers.origin;
77452
+ const port = this.server?.address();
77453
+ const expectedOrigin = `http://${host}:${port && typeof port !== "string" ? port.port : this.options.port}`;
77454
+ if (req.hostname !== host || origin && origin !== expectedOrigin) {
77455
+ res.status(403).json({ error: "Untrusted callback origin" });
77456
+ return;
77457
+ }
77458
+ next();
77459
+ });
77367
77460
  this.app.use(import_express.default.json());
77368
77461
  this.app.get("/oauth/callback", (req, res) => {
77369
77462
  logger.oauth("OAuth 2.1 authorization callback page requested");
@@ -77371,46 +77464,47 @@ var OAuthCallbackServer = class {
77371
77464
  });
77372
77465
  this.app.post("/oauth/callback", async (req, res) => {
77373
77466
  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
- });
77467
+ const { code, state, iss, error: error51 } = req.body;
77468
+ this.validateResponse(state, iss);
77469
+ if (error51) throw new OAuthError2("Authorization denied or failed", "AUTH_DENIED");
77470
+ if (this.options.flow === "implicit")
77471
+ throw new OAuthError2("Authorization-code callback is disabled", "WRONG_FLOW");
77472
+ if (typeof code !== "string" || !code)
77473
+ throw new OAuthError2("No authorization code received", "INVALID_CODE");
77384
77474
  this.events.emit("oauth-code-received", { code, state, iss });
77385
- res.json({ success: true, message: "Authorization code received successfully" });
77475
+ res.json({
77476
+ success: true,
77477
+ message: "Authorization code received; check connection status in the app"
77478
+ });
77386
77479
  } 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 });
77480
+ const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid authorization response");
77481
+ if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
77482
+ res.status(400).json({ error: failure.message });
77391
77483
  }
77392
77484
  });
77393
77485
  this.app.post("/oauth/tokens", async (req, res) => {
77486
+ if (this.options.flow !== "implicit") {
77487
+ res.status(404).json({ error: "Implicit token callback is disabled" });
77488
+ return;
77489
+ }
77394
77490
  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
- });
77491
+ this.validateResponse(req.body.state, req.body.iss);
77492
+ if (typeof req.body.access_token !== "string" || !req.body.access_token || req.body.token_type && req.body.token_type.toLowerCase() !== "bearer")
77493
+ throw new OAuthError2("Invalid implicit token response");
77494
+ const tokens = {
77495
+ access_token: req.body.access_token,
77496
+ token_type: "Bearer",
77497
+ expires_in: req.body.expires_in === void 0 ? void 0 : Number(req.body.expires_in),
77498
+ scope: req.body.scope,
77499
+ obtained_at: Date.now()
77500
+ };
77406
77501
  await writeTokens(this.options.serverUrlHash, tokens);
77407
77502
  this.events.emit("oauth-success", tokens);
77408
- res.json({ success: true, message: "Tokens saved successfully" });
77503
+ res.json({ success: true });
77409
77504
  } 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 });
77505
+ const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid implicit response");
77506
+ if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
77507
+ res.status(400).json({ error: failure.message });
77414
77508
  }
77415
77509
  });
77416
77510
  this.app.get("/oauth/health", (req, res) => {
@@ -77427,10 +77521,17 @@ var OAuthCallbackServer = class {
77427
77521
  }
77428
77522
  );
77429
77523
  }
77524
+ validateResponse(state, issuer) {
77525
+ if (!this.options.state || state !== this.options.state)
77526
+ throw new OAuthError2("Invalid callback state", "INVALID_STATE");
77527
+ if ((issuer !== void 0 || this.options.requireIssuer) && issuer !== this.options.issuer)
77528
+ throw new OAuthError2("Invalid callback issuer", "INVALID_ISSUER");
77529
+ }
77430
77530
  async start() {
77431
77531
  return new Promise((resolve, reject) => {
77432
77532
  try {
77433
77533
  this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
77534
+ clearTimeout(this.startupTimer);
77434
77535
  if (error51) {
77435
77536
  reject(error51);
77436
77537
  return;
@@ -77441,11 +77542,12 @@ var OAuthCallbackServer = class {
77441
77542
  resolve();
77442
77543
  });
77443
77544
  this.server.on("error", (error51) => {
77545
+ clearTimeout(this.startupTimer);
77444
77546
  logger.error("OAuth callback server error", "OAUTH", error51);
77445
77547
  reject(error51);
77446
77548
  });
77447
77549
  if (this.options.timeout) {
77448
- setTimeout(() => {
77550
+ this.startupTimer = setTimeout(() => {
77449
77551
  if (!this.server?.listening) {
77450
77552
  reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
77451
77553
  }
@@ -77457,6 +77559,7 @@ var OAuthCallbackServer = class {
77457
77559
  });
77458
77560
  }
77459
77561
  async stop() {
77562
+ clearTimeout(this.startupTimer);
77460
77563
  if (!this.server) return;
77461
77564
  const server = this.server;
77462
77565
  this.server = null;
@@ -77488,7 +77591,9 @@ var MCPOAuthProvider = class {
77488
77591
  this.authServerMetadata = null;
77489
77592
  this.resourceMetadata = null;
77490
77593
  const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
77491
- const defaultScopes = getDefaultOAuthScopes();
77594
+ const defaultScopes = CONFIG.OAUTH_SCOPES ? getDefaultOAuthScopes() : [];
77595
+ this.explicitScopes = options.scopes ?? defaultScopes;
77596
+ this.configuredClientId = options.clientId || CONFIG.WP_OAUTH_CLIENT_ID || void 0;
77492
77597
  this.config = {
77493
77598
  serverUrl,
77494
77599
  resource: generateCanonicalResourceURI(serverUrl),
@@ -77547,187 +77652,217 @@ var MCPOAuthProvider = class {
77547
77652
  /**
77548
77653
  * Discover OAuth 2.1 server endpoints using MCP-compliant discovery
77549
77654
  */
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
- }
77655
+ async discoverOAuthEndpoints(challenge) {
77656
+ this.resourceMetadata = await discoverProtectedResourceMetadata(
77657
+ this.config.serverUrl,
77658
+ challenge?.resource_metadata
77659
+ );
77660
+ this.authServerMetadata = await discoverAuthorizationServerMetadata(
77661
+ this.resourceMetadata.authorization_servers[0]
77662
+ );
77663
+ this.config.resource = this.resourceMetadata.resource;
77664
+ this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
77665
+ this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
77666
+ this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
77667
+ this.config.clientId = this.configuredClientId;
77668
+ const challenged = challenge?.scope?.split(/\s+/).filter(Boolean);
77669
+ this.config.scopes = [
77670
+ ...new Set(
77671
+ challenged ?? (this.explicitScopes.length ? this.explicitScopes : this.resourceMetadata.scopes_supported ?? [])
77672
+ )
77673
+ ];
77613
77674
  }
77614
- /**
77615
- * Get authorization server URL from discovered metadata
77616
- */
77617
77675
  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
- }
77676
+ return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
77627
77677
  }
77628
77678
  /**
77629
77679
  * Perform dynamic client registration if needed and supported
77630
77680
  */
77631
77681
  async ensureClientRegistration() {
77682
+ const issuer = this.getAuthorizationServerUrl();
77683
+ const stored = await readClientInfo(this.serverUrlHash);
77632
77684
  if (this.config.clientId) {
77633
- logger.oauth("Using provided client ID, skipping dynamic registration");
77685
+ if (stored?.issuer && stored.issuer !== issuer && stored.client_id === this.config.clientId) {
77686
+ throw new OAuthError2(
77687
+ "Configured client belongs to another authorization server; update its registration",
77688
+ "CLIENT_ISSUER_MISMATCH"
77689
+ );
77690
+ }
77691
+ await writeClientInfo(this.serverUrlHash, { client_id: this.config.clientId, issuer });
77634
77692
  return;
77635
77693
  }
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");
77694
+ if (stored?.issuer === issuer && typeof stored.client_id === "string" && stored.client_id.length > 0) {
77695
+ this.config.clientId = stored.client_id;
77640
77696
  return;
77641
77697
  }
77642
- if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION) {
77698
+ if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
77643
77699
  throw new OAuthError2(
77644
- "No client ID provided and dynamic registration is disabled",
77700
+ "No issuer-bound client registration; configure a client ID or enable registration",
77645
77701
  "NO_CLIENT_ID"
77646
77702
  );
77647
- }
77648
- if (!this.authServerMetadata?.registration_endpoint) {
77703
+ if (!this.authServerMetadata?.registration_endpoint)
77649
77704
  throw new OAuthError2(
77650
- "Dynamic client registration not supported by authorization server",
77705
+ "Authorization server does not support dynamic registration; configure its client ID",
77651
77706
  "NO_DYNAMIC_REGISTRATION"
77652
77707
  );
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
- }
77708
+ const request = {
77709
+ redirect_uris: [this.config.redirectUri],
77710
+ application_type: "native",
77711
+ token_endpoint_auth_method: "none",
77712
+ grant_types: ["authorization_code", "refresh_token"],
77713
+ response_types: ["code"],
77714
+ client_name: "WordPress MCP Remote",
77715
+ ...this.config.scopes.length ? { scope: this.config.scopes.join(" ") } : {}
77716
+ };
77717
+ const registered = await registerDynamicClient(
77718
+ this.authServerMetadata.registration_endpoint,
77719
+ request
77720
+ );
77721
+ await writeClientInfo(this.serverUrlHash, {
77722
+ ...request,
77723
+ client_id: registered.client_id,
77724
+ issuer
77725
+ });
77726
+ this.config.clientId = registered.client_id;
77680
77727
  }
77681
77728
  /**
77682
77729
  * Initiate MCP-compliant OAuth 2.1 authorization flow
77683
77730
  */
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;
77731
+ satisfies(tokens, request) {
77732
+ return !!tokens && tokens.access_token !== request.rejectedToken && (!request.scopes?.length || request.scopes.every((scope) => tokens.scope?.split(" ").includes(scope)));
77733
+ }
77734
+ async authorize(request = {}) {
77735
+ if (this.authPromise) await this.authPromise;
77736
+ if (!this.authPromise) this.authPromise = this.authorizeWithLock(request);
77737
+ const pending = this.authPromise;
77738
+ try {
77739
+ await pending;
77740
+ } finally {
77741
+ if (this.authPromise === pending) this.authPromise = null;
77694
77742
  }
77695
- logger.oauth("Starting MCP-compliant OAuth 2.1 authorization flow");
77696
- this.authPromise = this.performAuthorization();
77743
+ }
77744
+ async authorizeWithLock(request) {
77745
+ const valid = await this.tokens();
77746
+ if (this.satisfies(valid, request)) return valid;
77747
+ await ensureConfigDir();
77748
+ const file2 = getConfigFilePath(this.serverUrlHash, "oauth");
77749
+ let release;
77697
77750
  try {
77698
- await this.authPromise;
77699
- logger.oauth("OAuth 2.1 authorization completed successfully");
77751
+ release = await lockfile.lock(file2, { realpath: false });
77700
77752
  } catch (error51) {
77701
- logger.error("OAuth 2.1 authorization failed", "OAUTH", error51);
77702
- throw error51;
77753
+ if (error51.code !== "ELOCKED") throw error51;
77754
+ const deadline = Date.now() + this.config.timeout;
77755
+ while (Date.now() < deadline) {
77756
+ const tokens = await this.tokens();
77757
+ if (this.satisfies(tokens, request)) return tokens;
77758
+ if (!await lockfile.check(file2, { realpath: false })) {
77759
+ const completed = await this.tokens();
77760
+ if (this.satisfies(completed, request)) return completed;
77761
+ throw new OAuthError2("Authentication in another process did not complete", "AUTH_FAILED");
77762
+ }
77763
+ await delay(Math.min(100, Math.max(0, deadline - Date.now())));
77764
+ }
77765
+ throw new OAuthError2("Timeout waiting for authentication in another process", "LOCK_TIMEOUT");
77766
+ }
77767
+ try {
77768
+ const completed = await this.tokens();
77769
+ if (this.satisfies(completed, request)) return completed;
77770
+ await this.discoverOAuthEndpoints(request.challenge);
77771
+ const issuer = this.getAuthorizationServerUrl();
77772
+ const stored = await readTokens(this.serverUrlHash);
77773
+ const client = await readClientInfo(this.serverUrlHash);
77774
+ if (this.configuredClientId && client?.issuer && client.issuer !== issuer && client.client_id === this.configuredClientId) {
77775
+ throw new OAuthError2(
77776
+ "Configured client belongs to another authorization server",
77777
+ "CLIENT_ISSUER_MISMATCH"
77778
+ );
77779
+ }
77780
+ this.config.scopes = [
77781
+ .../* @__PURE__ */ new Set([
77782
+ ...stored?.issuer === issuer ? stored.scope?.split(" ").filter(Boolean) ?? [] : [],
77783
+ ...this.config.scopes,
77784
+ ...request.scopes ?? []
77785
+ ])
77786
+ ];
77787
+ const needsScope = !!request.scopes?.some(
77788
+ (scope) => !stored?.scope?.split(" ").includes(scope)
77789
+ );
77790
+ const clientId = this.configuredClientId || client?.client_id;
77791
+ if (stored?.issuer === issuer && client?.issuer === issuer && stored.refresh_token && clientId && (!this.configuredClientId || this.configuredClientId === client.client_id) && !needsScope) {
77792
+ try {
77793
+ const renewed = await refreshAuthorization(issuer, {
77794
+ metadata: { ...this.authServerMetadata },
77795
+ clientInformation: { client_id: clientId },
77796
+ refreshToken: stored.refresh_token,
77797
+ resource: this.resourceIndicator(),
77798
+ fetchFn: (url2, init) => oauthFetch(String(url2), init)
77799
+ });
77800
+ if (renewed.token_type.toLowerCase() !== "bearer")
77801
+ throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
77802
+ const tokens = {
77803
+ ...renewed,
77804
+ issuer,
77805
+ scope: renewed.scope ?? stored.scope,
77806
+ obtained_at: Date.now()
77807
+ };
77808
+ await writeTokens(this.serverUrlHash, tokens);
77809
+ return tokens;
77810
+ } catch (error51) {
77811
+ if (!(error51 instanceof InvalidGrantError)) throw error51;
77812
+ await deleteConfigFile(this.serverUrlHash, "tokens.json");
77813
+ }
77814
+ }
77815
+ return await this.performAuthorization();
77703
77816
  } finally {
77704
- this.authPromise = null;
77817
+ await release();
77705
77818
  }
77706
77819
  }
77820
+ resourceIndicator() {
77821
+ if (!CONFIG.OAUTH_RESOURCE_INDICATOR)
77822
+ throw new OAuthError2(
77823
+ "MCP OAuth requires resource indicators; remove OAUTH_RESOURCE_INDICATOR=false",
77824
+ "RESOURCE_REQUIRED"
77825
+ );
77826
+ return this.config.resource;
77827
+ }
77707
77828
  /**
77708
77829
  * Perform the complete OAuth 2.1 authorization flow
77709
77830
  */
77710
77831
  async performAuthorization() {
77711
77832
  try {
77712
- await this.discoverOAuthEndpoints();
77713
- const callbackPort = this.config.callbackPort === 0 ? await getOAuthCallbackPort() : this.config.callbackPort;
77833
+ if (!this.authServerMetadata) await this.discoverOAuthEndpoints();
77834
+ if (!this.authServerMetadata?.code_challenge_methods_supported?.includes("S256"))
77835
+ throw new OAuthError2(
77836
+ "Authorization server must advertise PKCE S256 support",
77837
+ "PKCE_REQUIRED"
77838
+ );
77839
+ this.resourceIndicator();
77840
+ if (!["127.0.0.1", "localhost", "::1"].includes(this.config.host))
77841
+ throw new OAuthError2(
77842
+ "Native OAuth callback must bind to loopback",
77843
+ "INVALID_CALLBACK_HOST"
77844
+ );
77845
+ this.currentPKCE = generatePKCE();
77846
+ this.currentState = generateSecureState();
77847
+ const callbackPort = this.config.callbackPort === 0 ? await selectCallbackPort(this.config.serverUrl, void 0, false, this.config.host) : this.config.callbackPort;
77714
77848
  const callbackServer = setupWPOAuthCallbackServer(
77715
77849
  {
77716
77850
  port: callbackPort,
77717
77851
  host: this.config.host,
77718
77852
  serverUrlHash: this.serverUrlHash,
77719
- timeout: this.config.timeout
77853
+ timeout: this.config.timeout,
77854
+ state: this.currentState,
77855
+ issuer: this.authServerMetadata.issuer,
77856
+ requireIssuer: this.authServerMetadata.authorization_response_iss_parameter_supported
77720
77857
  },
77721
77858
  this.events
77722
77859
  );
77723
77860
  await callbackServer.start();
77724
77861
  logger.oauth("OAuth callback server started");
77725
77862
  try {
77726
- const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
77863
+ const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
77727
77864
  this.config.redirectUri = actualRedirectUri;
77728
77865
  await this.ensureClientRegistration();
77729
- this.currentPKCE = generatePKCE();
77730
- this.currentState = generateSecureState();
77731
77866
  await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
77732
77867
  await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
77733
77868
  const authUrl = buildAuthorizationUrl(
@@ -77737,10 +77872,13 @@ var MCPOAuthProvider = class {
77737
77872
  this.config.scopes,
77738
77873
  this.currentState,
77739
77874
  this.currentPKCE.codeChallenge,
77740
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77875
+ this.resourceIndicator()
77741
77876
  );
77742
77877
  logger.oauth("Built OAuth 2.1 authorization URL");
77743
77878
  logger.debug("Authorization URL", "OAUTH", { url: authUrl });
77879
+ const authorizationCode = this.waitForAuthorizationCode();
77880
+ void authorizationCode.catch(() => {
77881
+ });
77744
77882
  try {
77745
77883
  await open_default(authUrl);
77746
77884
  logger.oauth("Browser opened successfully");
@@ -77751,13 +77889,20 @@ var MCPOAuthProvider = class {
77751
77889
  logger.info(`${authUrl}`);
77752
77890
  logger.info("===============================\n");
77753
77891
  }
77754
- const authCode = await this.waitForAuthorizationCode();
77755
- const tokens = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
77892
+ const authCode = await authorizationCode;
77893
+ const response = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
77894
+ const tokens = {
77895
+ ...response,
77896
+ issuer: this.getAuthorizationServerUrl(),
77897
+ scope: response.scope ?? this.config.scopes.join(" ")
77898
+ };
77756
77899
  await writeTokens(this.serverUrlHash, tokens);
77757
77900
  logger.oauth("OAuth 2.1 tokens stored successfully");
77758
77901
  return tokens;
77759
77902
  } finally {
77760
77903
  await callbackServer.stop();
77904
+ await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77905
+ await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
77761
77906
  }
77762
77907
  } catch (error51) {
77763
77908
  logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
@@ -77778,12 +77923,16 @@ var MCPOAuthProvider = class {
77778
77923
  this.events.removeAllListeners("oauth-error");
77779
77924
  clearTimeout(timeout);
77780
77925
  };
77781
- this.events.once("oauth-code-received", async ({ code, state }) => {
77926
+ this.events.once("oauth-code-received", async ({ code, state, iss }) => {
77782
77927
  cleanup();
77783
77928
  if (state !== this.currentState) {
77784
77929
  reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
77785
77930
  return;
77786
77931
  }
77932
+ if ((iss !== void 0 || this.authServerMetadata?.authorization_response_iss_parameter_supported) && iss !== this.authServerMetadata?.issuer) {
77933
+ reject(new OAuthError2("Authorization response issuer mismatch", "INVALID_ISSUER"));
77934
+ return;
77935
+ }
77787
77936
  logger.oauth("Valid authorization code received");
77788
77937
  resolve(code);
77789
77938
  });
@@ -77809,7 +77958,7 @@ var MCPOAuthProvider = class {
77809
77958
  redirectUri,
77810
77959
  this.config.clientId,
77811
77960
  codeVerifier,
77812
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77961
+ this.resourceIndicator()
77813
77962
  );
77814
77963
  await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77815
77964
  await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
@@ -77875,6 +78024,7 @@ import * as path5 from "node:path";
77875
78024
  import * as os3 from "node:os";
77876
78025
 
77877
78026
  // src/lib/persistent-oauth-client-provider.ts
78027
+ import { randomBytes } from "node:crypto";
77878
78028
  import { EventEmitter as EventEmitter2 } from "node:events";
77879
78029
  var PersistentWPOAuthClientProvider = class {
77880
78030
  constructor(options) {
@@ -78032,7 +78182,10 @@ var PersistentWPOAuthClientProvider = class {
78032
78182
  */
78033
78183
  async performAuthorization() {
78034
78184
  const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
78185
+ const state = this.generateState();
78035
78186
  const callbackServerOptions = {
78187
+ flow: "implicit",
78188
+ state,
78036
78189
  port: callbackPort,
78037
78190
  host: this.options.host,
78038
78191
  serverUrlHash: this.serverUrlHash,
@@ -78044,12 +78197,14 @@ var PersistentWPOAuthClientProvider = class {
78044
78197
  logger.oauth("Starting callback server...");
78045
78198
  await callbackServer.start();
78046
78199
  logger.oauth("Callback server started successfully");
78047
- const state = this.generateState();
78048
78200
  logger.debug(`Generated state parameter: ${state}`, "OAUTH");
78049
78201
  const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
78050
78202
  logger.oauth(`Built authorization URL: ${authUrl}`);
78051
78203
  logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
78052
78204
  logger.oauth("Attempting to open browser...");
78205
+ const authorization = this.waitForAuthorizationResult();
78206
+ void authorization.catch(() => {
78207
+ });
78053
78208
  try {
78054
78209
  await open_default(authUrl);
78055
78210
  logger.oauth("Browser opened successfully");
@@ -78061,7 +78216,7 @@ var PersistentWPOAuthClientProvider = class {
78061
78216
  logger.info("===============================\n");
78062
78217
  }
78063
78218
  logger.oauth("Waiting for authorization result...");
78064
- const tokens = await this.waitForAuthorizationResult();
78219
+ const tokens = await authorization;
78065
78220
  logger.oauth("Authorization result received");
78066
78221
  logger.oauth("Saving tokens to persistent storage...");
78067
78222
  await this.saveTokens(tokens);
@@ -78114,7 +78269,7 @@ var PersistentWPOAuthClientProvider = class {
78114
78269
  * Generate a secure state parameter
78115
78270
  */
78116
78271
  generateState() {
78117
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
78272
+ return randomBytes(32).toString("base64url");
78118
78273
  }
78119
78274
  /**
78120
78275
  * Wait for authorization result from callback server
@@ -78537,6 +78692,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
78537
78692
  return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
78538
78693
  }
78539
78694
  }
78695
+ function getMCPOAuthProvider() {
78696
+ return mcpOAuthProvider ??= new MCPOAuthProvider({
78697
+ serverUrl: getRequestUrl(),
78698
+ clientId: CONFIG.WP_OAUTH_CLIENT_ID
78699
+ });
78700
+ }
78540
78701
  async function getOAuthTokens() {
78541
78702
  try {
78542
78703
  if (!CONFIG.OAUTH_ENABLED) {
@@ -78544,7 +78705,7 @@ async function getOAuthTokens() {
78544
78705
  return null;
78545
78706
  }
78546
78707
  logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
78547
- const serverUrl = CONFIG.WP_API_URL;
78708
+ const serverUrl = getRequestUrl();
78548
78709
  const serverUrlHash = generateServerUrlHash(serverUrl);
78549
78710
  const existingTokens = await getValidTokens(serverUrlHash);
78550
78711
  if (existingTokens) {
@@ -78555,16 +78716,10 @@ async function getOAuthTokens() {
78555
78716
  logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
78556
78717
  logger.auth("Your browser should open automatically for authentication");
78557
78718
  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
78719
  logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
78566
- await mcpOAuthProvider.authorize();
78567
- const tokens = await mcpOAuthProvider.tokens();
78720
+ const provider = getMCPOAuthProvider();
78721
+ await provider.authorize();
78722
+ const tokens = await provider.tokens();
78568
78723
  if (tokens) {
78569
78724
  logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
78570
78725
  return tokens;
@@ -78661,7 +78816,7 @@ function updateSessionId(sessionId) {
78661
78816
  globalSessionId = sessionId;
78662
78817
  logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
78663
78818
  }
78664
- async function getWordPressHeaders(requestData) {
78819
+ async function getWordPressHeaders(requestData, options = {}) {
78665
78820
  validateEnvironment();
78666
78821
  let authHeader = "";
78667
78822
  if (CONFIG.JWT_TOKEN) {
@@ -78670,7 +78825,7 @@ async function getWordPressHeaders(requestData) {
78670
78825
  logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
78671
78826
  } else if (CONFIG.OAUTH_ENABLED) {
78672
78827
  logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
78673
- const oauthTokens = await getOAuthTokens();
78828
+ const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
78674
78829
  if (oauthTokens) {
78675
78830
  authHeader = `Bearer ${oauthTokens.access_token}`;
78676
78831
  logger.auth("Using OAuth token authentication for WordPress API");
@@ -78710,7 +78865,7 @@ async function getWordPressHeaders(requestData) {
78710
78865
  if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
78711
78866
  }
78712
78867
  const hasCustomHeaders = Object.keys(customHeaders).length > 0;
78713
- if (!authHeader && !hasCustomHeaders) {
78868
+ if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
78714
78869
  throw new AuthError(
78715
78870
  "No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
78716
78871
  "NO_AUTH_METHOD"