@automattic/mcp-wordpress-remote 0.5.1 → 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
@@ -73824,10 +73824,9 @@ var ProgressTokenSchema = union([string2(), number2().int()]);
73824
73824
  var CursorSchema = string2();
73825
73825
  var TaskCreationParamsSchema = looseObject({
73826
73826
  /**
73827
- * Time in milliseconds to keep task results available after completion.
73828
- * If null, the task has unlimited lifetime until manually cleaned up.
73827
+ * Requested duration in milliseconds to retain task from creation.
73829
73828
  */
73830
- ttl: union([number2(), _null3()]).optional(),
73829
+ ttl: number2().optional(),
73831
73830
  /**
73832
73831
  * Time in milliseconds to wait between task status requests.
73833
73832
  */
@@ -74122,7 +74121,11 @@ var ClientCapabilitiesSchema = object({
74122
74121
  /**
74123
74122
  * Present if the client supports task creation.
74124
74123
  */
74125
- tasks: ClientTasksCapabilitySchema.optional()
74124
+ tasks: ClientTasksCapabilitySchema.optional(),
74125
+ /**
74126
+ * Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
74127
+ */
74128
+ extensions: record(string2(), AssertObjectSchema).optional()
74126
74129
  });
74127
74130
  var InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
74128
74131
  /**
@@ -74183,7 +74186,11 @@ var ServerCapabilitiesSchema = object({
74183
74186
  /**
74184
74187
  * Present if the server supports task creation.
74185
74188
  */
74186
- tasks: ServerTasksCapabilitySchema.optional()
74189
+ tasks: ServerTasksCapabilitySchema.optional(),
74190
+ /**
74191
+ * Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name).
74192
+ */
74193
+ extensions: record(string2(), AssertObjectSchema).optional()
74187
74194
  });
74188
74195
  var InitializeResultSchema = ResultSchema.extend({
74189
74196
  /**
@@ -74375,6 +74382,12 @@ var ResourceSchema = object({
74375
74382
  * The MIME type of this resource, if known.
74376
74383
  */
74377
74384
  mimeType: optional(string2()),
74385
+ /**
74386
+ * The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
74387
+ *
74388
+ * This can be used by Hosts to display file sizes and estimate context window usage.
74389
+ */
74390
+ size: optional(number2()),
74378
74391
  /**
74379
74392
  * Optional annotations for the client.
74380
74393
  */
@@ -75393,7 +75406,12 @@ var OAuthTokensSchema = object({
75393
75406
  token_type: string2(),
75394
75407
  expires_in: coerce_exports.number().optional(),
75395
75408
  scope: string2().optional(),
75396
- refresh_token: string2().optional()
75409
+ refresh_token: string2().optional(),
75410
+ /**
75411
+ * Not part of the wire format: the authorization server this value was obtained from, added by
75412
+ * the client's `auth()` before it is stored and compared when it is read back.
75413
+ */
75414
+ issuer: string2().optional().catch(void 0)
75397
75415
  }).strip();
75398
75416
  var OAuthErrorResponseSchema = object({
75399
75417
  error: string2(),
@@ -75423,7 +75441,12 @@ var OAuthClientInformationSchema = object({
75423
75441
  client_id: string2(),
75424
75442
  client_secret: string2().optional(),
75425
75443
  client_id_issued_at: number2().optional(),
75426
- client_secret_expires_at: number2().optional()
75444
+ client_secret_expires_at: number2().optional(),
75445
+ /**
75446
+ * Not part of the wire format: the authorization server this value was obtained from, added by
75447
+ * the client's `auth()` before it is stored and compared when it is read back.
75448
+ */
75449
+ issuer: string2().optional().catch(void 0)
75427
75450
  }).strip();
75428
75451
  var OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
75429
75452
  var OAuthClientRegistrationErrorSchema = object({
@@ -75530,6 +75553,154 @@ var OAUTH_ERRORS = {
75530
75553
  [InvalidTargetError.errorCode]: InvalidTargetError
75531
75554
  };
75532
75555
 
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
+ }
75624
+ var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
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
+ }
75703
+
75533
75704
  // src/lib/utils.ts
75534
75705
  import * as fs2 from "node:fs";
75535
75706
  import * as path3 from "node:path";
@@ -75641,7 +75812,19 @@ async function writeTokens(serverUrlHash, tokens) {
75641
75812
  ...tokens,
75642
75813
  obtained_at: Date.now()
75643
75814
  };
75644
- 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
+ }
75645
75828
  logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
75646
75829
  } catch (error51) {
75647
75830
  logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
@@ -75674,7 +75857,7 @@ function isTokenValid(tokens) {
75674
75857
  if (!tokens?.access_token) {
75675
75858
  return { isValid: false, error: "No access token" };
75676
75859
  }
75677
- if (!tokens.expires_in || !tokens.obtained_at) {
75860
+ if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
75678
75861
  return { isValid: true };
75679
75862
  }
75680
75863
  const now = Date.now();
@@ -75713,12 +75896,12 @@ var OAuthClientInfoSchema = external_exports.object({
75713
75896
  client_id: external_exports.string().optional(),
75714
75897
  client_secret: external_exports.string().optional()
75715
75898
  });
75716
- async function findAvailablePort(preferredPort) {
75899
+ async function findAvailablePort(preferredPort, host = "127.0.0.1") {
75717
75900
  return new Promise((resolve, reject) => {
75718
75901
  const server = net.createServer();
75719
75902
  server.on("error", (err) => {
75720
75903
  if (err.code === "EADDRINUSE") {
75721
- server.listen(0);
75904
+ server.listen(0, host);
75722
75905
  } else {
75723
75906
  reject(err);
75724
75907
  }
@@ -75729,7 +75912,7 @@ async function findAvailablePort(preferredPort) {
75729
75912
  resolve(port);
75730
75913
  });
75731
75914
  });
75732
- server.listen(preferredPort || 0);
75915
+ server.listen(preferredPort || 0, host);
75733
75916
  });
75734
75917
  }
75735
75918
  function calculateDefaultPort(serverUrlHash) {
@@ -75757,24 +75940,17 @@ async function findExistingClientPort(serverUrlHash) {
75757
75940
  function getServerUrlHash(serverUrl) {
75758
75941
  return crypto3.createHash("md5").update(serverUrl).digest("hex");
75759
75942
  }
75760
- async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
75943
+ async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
75761
75944
  const serverUrlHash = getServerUrlHash(serverUrl);
75762
75945
  if (specifiedPort) {
75763
75946
  return specifiedPort;
75764
75947
  }
75765
- const defaultPort = calculateDefaultPort(serverUrlHash);
75766
- const [existingClientPort, availablePort] = await Promise.all([
75767
- findExistingClientPort(serverUrlHash),
75768
- findAvailablePort(defaultPort)
75769
- ]);
75770
- if (existingClientPort) {
75771
- return existingClientPort;
75772
- }
75773
- return availablePort;
75948
+ const existingClientPort = await findExistingClientPort(serverUrlHash);
75949
+ return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
75774
75950
  }
75775
75951
 
75776
75952
  // src/lib/config.ts
75777
- var MCP_WORDPRESS_REMOTE_VERSION = "0.5.1";
75953
+ var MCP_WORDPRESS_REMOTE_VERSION = "0.5.3";
75778
75954
  function parsePositiveIntEnv(value, defaultValue) {
75779
75955
  if (!value) {
75780
75956
  return defaultValue;
@@ -75975,8 +76151,8 @@ async function getOAuthCallbackPort() {
75975
76151
  return selectCallbackPort(
75976
76152
  CONFIG.WP_API_URL,
75977
76153
  CONFIG.OAUTH_CALLBACK_PORT,
75978
- false
75979
- // Always treat as self-hosted for port selection
76154
+ false,
76155
+ CONFIG.OAUTH_HOST
75980
76156
  );
75981
76157
  }
75982
76158
  function formatConfigError(error51) {
@@ -76896,10 +77072,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
76896
77072
  var open_default = open;
76897
77073
 
76898
77074
  // src/lib/mcp-oauth-provider.ts
77075
+ import lockfile from "proper-lockfile";
77076
+ import { setTimeout as delay } from "node:timers/promises";
76899
77077
  import { EventEmitter } from "node:events";
76900
77078
 
76901
77079
  // src/lib/mcp-oauth-utils.ts
76902
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
76903
77117
  function generatePKCE() {
76904
77118
  const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
76905
77119
  const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
@@ -76927,118 +77141,76 @@ function generateCanonicalResourceURI(serverUrl) {
76927
77141
  }
76928
77142
  }
76929
77143
  async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
76930
- try {
76931
- const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationServerUrl);
76932
- logger.oauth(`Discovering authorization server metadata: ${metadataUrl}`);
76933
- const customHeaders = getCustomHeaders();
76934
- const response = await proxyFetch(metadataUrl.toString(), {
76935
- headers: {
76936
- Accept: "application/json",
76937
- ...customHeaders
76938
- }
76939
- });
76940
- 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)
76941
77150
  throw new OAuthError2(
76942
- `Failed to fetch authorization server metadata: ${response.status}`,
77151
+ `Metadata request failed (${response.status})`,
76943
77152
  "METADATA_DISCOVERY_FAILED"
76944
77153
  );
76945
- }
76946
- const metadata = await response.json();
76947
- if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
77154
+ const parsed = OAuthMetadataSchema.safeParse(await response.json());
77155
+ if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
76948
77156
  throw new OAuthError2(
76949
- "Authorization server metadata missing required endpoints",
77157
+ "Authorization metadata has an invalid or mismatched issuer",
76950
77158
  "INVALID_METADATA"
76951
77159
  );
76952
77160
  }
76953
- 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);
76954
77166
  return metadata;
76955
- } catch (error51) {
76956
- if (error51 instanceof OAuthError2) {
76957
- throw error51;
76958
- }
76959
- throw new OAuthError2(
76960
- `Error discovering authorization server metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
76961
- "METADATA_DISCOVERY_ERROR"
76962
- );
76963
77167
  }
77168
+ throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
76964
77169
  }
76965
- async function discoverProtectedResourceMetadata(resourceUrl) {
76966
- try {
76967
- const url2 = new URL(resourceUrl);
76968
- const metadataUrl = new URL("/.well-known/oauth-protected-resource", url2.origin);
76969
- logger.oauth(`Discovering protected resource metadata: ${metadataUrl}`);
76970
- const customHeaders = getCustomHeaders();
76971
- const response = await proxyFetch(metadataUrl.toString(), {
76972
- headers: {
76973
- Accept: "application/json",
76974
- ...customHeaders
76975
- }
76976
- });
76977
- 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)
76978
77188
  throw new OAuthError2(
76979
- `Failed to fetch protected resource metadata: ${response.status}`,
77189
+ `Resource metadata request failed (${response.status})`,
76980
77190
  "RESOURCE_METADATA_DISCOVERY_FAILED"
76981
77191
  );
76982
- }
76983
- const metadata = await response.json();
76984
- 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) {
76985
77194
  throw new OAuthError2(
76986
- "Protected resource metadata missing authorization servers",
77195
+ "Protected resource metadata has an invalid or mismatched resource",
76987
77196
  "INVALID_RESOURCE_METADATA"
76988
77197
  );
76989
77198
  }
76990
- logger.oauth("Protected resource metadata discovered successfully");
76991
- return metadata;
76992
- } catch (error51) {
76993
- if (error51 instanceof OAuthError2) {
76994
- throw error51;
76995
- }
76996
- throw new OAuthError2(
76997
- `Error discovering protected resource metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
76998
- "RESOURCE_METADATA_DISCOVERY_ERROR"
76999
- );
77199
+ for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
77200
+ return parsed.data;
77000
77201
  }
77001
- }
77002
- function parseWWWAuthenticateHeader(headerValue) {
77003
- const parts = headerValue.split(/\s+/);
77004
- const scheme = parts[0];
77005
- const result = { scheme };
77006
- const paramString = parts.slice(1).join(" ");
77007
- const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
77008
- for (const match of paramMatches) {
77009
- const [, key, value] = match;
77010
- switch (key) {
77011
- case "realm":
77012
- result.realm = value;
77013
- break;
77014
- case "scope":
77015
- result.scope = value;
77016
- break;
77017
- case "error":
77018
- result.error = value;
77019
- break;
77020
- case "error_description":
77021
- result.error_description = value;
77022
- break;
77023
- case "error_uri":
77024
- result.error_uri = value;
77025
- break;
77026
- case "resource_metadata_url":
77027
- result.resource_metadata_url = value;
77028
- break;
77029
- }
77030
- }
77031
- return result;
77202
+ throw new OAuthError2(
77203
+ "No protected resource metadata found",
77204
+ "RESOURCE_METADATA_DISCOVERY_FAILED"
77205
+ );
77032
77206
  }
77033
77207
  async function registerDynamicClient(registrationEndpoint, registrationRequest) {
77034
77208
  try {
77035
77209
  logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
77036
- const customHeaders = getCustomHeaders();
77037
- const response = await proxyFetch(registrationEndpoint, {
77210
+ const response = await oauthFetch(registrationEndpoint, {
77038
77211
  method: "POST",
77039
77212
  headers: {
77040
- "Content-Type": "application/json",
77041
- ...customHeaders
77213
+ "Content-Type": "application/json"
77042
77214
  },
77043
77215
  body: JSON.stringify(registrationRequest)
77044
77216
  });
@@ -77050,12 +77222,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
77050
77222
  );
77051
77223
  }
77052
77224
  const registrationResponse = await response.json();
77053
- if (!registrationResponse.client_id) {
77225
+ if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
77054
77226
  throw new OAuthError2(
77055
77227
  "Dynamic client registration response missing client_id",
77056
77228
  "INVALID_REGISTRATION_RESPONSE"
77057
77229
  );
77058
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
+ }
77059
77245
  logger.oauth("Dynamic client registration successful");
77060
77246
  logger.debug("Client ID obtained", "OAUTH", {
77061
77247
  client_id: registrationResponse.client_id,
@@ -77085,12 +77271,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77085
77271
  if (resource) {
77086
77272
  params.set("resource", resource);
77087
77273
  }
77088
- const customHeaders = getCustomHeaders();
77089
- const response = await proxyFetch(tokenEndpoint, {
77274
+ const response = await oauthFetch(tokenEndpoint, {
77090
77275
  method: "POST",
77091
77276
  headers: {
77092
- "Content-Type": "application/x-www-form-urlencoded",
77093
- ...customHeaders
77277
+ "Content-Type": "application/x-www-form-urlencoded"
77094
77278
  },
77095
77279
  body: params.toString()
77096
77280
  });
@@ -77101,7 +77285,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
77101
77285
  "TOKEN_EXCHANGE_FAILED"
77102
77286
  );
77103
77287
  }
77104
- 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");
77105
77291
  if (!tokenResponse.access_token) {
77106
77292
  throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
77107
77293
  }
@@ -77130,15 +77316,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
77130
77316
  // OAuth 2.1 uses authorization code flow
77131
77317
  client_id: clientId,
77132
77318
  redirect_uri: redirectUri,
77133
- scope: scopes.join(" "),
77134
77319
  state,
77135
77320
  code_challenge: codeChallenge,
77136
77321
  code_challenge_method: "S256"
77137
77322
  });
77323
+ if (scopes.length) params.set("scope", scopes.join(" "));
77138
77324
  if (resource) {
77139
77325
  params.set("resource", resource);
77140
77326
  }
77141
- 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();
77142
77331
  }
77143
77332
  function generateSecureState() {
77144
77333
  return crypto4.randomBytes(32).toString("base64url");
@@ -77219,110 +77408,26 @@ var AUTHORIZATION_CODE_HTML = `
77219
77408
  </div>
77220
77409
 
77221
77410
  <script>
77222
- function displayStatus(message, type = 'loading') {
77223
- const statusEl = document.getElementById('status');
77224
- const spinnerEl = document.getElementById('spinner');
77225
-
77226
- statusEl.textContent = message;
77227
- statusEl.className = type;
77228
-
77229
- if (type !== 'loading') {
77230
- spinnerEl.style.display = 'none';
77231
- }
77232
- }
77233
-
77234
- function displayDetails(message) {
77235
- document.getElementById('details').textContent = message;
77236
- }
77237
-
77238
- // Handle both OAuth 2.1 authorization code flow and implicit flow
77239
- const urlParams = new URLSearchParams(window.location.search);
77240
- const hashParams = new URLSearchParams(window.location.hash.substring(1));
77241
-
77242
- // Check for authorization code first (OAuth 2.1 flow)
77243
- if (urlParams.has('code')) {
77244
- const authData = {
77245
- code: urlParams.get('code'),
77246
- state: urlParams.get('state'),
77247
- // Additional parameters for validation
77248
- iss: urlParams.get('iss'),
77249
- };
77250
-
77251
- // Send authorization code to server for token exchange
77252
- fetch('/oauth/callback', {
77253
- method: 'POST',
77254
- headers: {
77255
- 'Content-Type': 'application/json'
77256
- },
77257
- body: JSON.stringify(authData)
77258
- })
77259
- .then(response => {
77260
- if (response.ok) {
77261
- displayStatus('\u2705 Authorization successful!', 'success');
77262
- displayDetails('You can safely close this window.');
77263
- } else {
77264
- return response.json().then(err => {
77265
- throw new Error(err.error || 'Failed to exchange authorization code');
77266
- });
77267
- }
77268
- })
77269
- .catch(error => {
77270
- displayStatus('\u274C Error completing authorization', 'error');
77271
- displayDetails(error.message + '. Please try again.');
77272
- });
77273
- }
77274
- // Check for access token in URL fragment (implicit flow)
77275
- else if (hashParams.has('access_token')) {
77276
- const tokens = {
77277
- access_token: hashParams.get('access_token'),
77278
- token_type: hashParams.get('token_type') || 'Bearer',
77279
- expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600,
77280
- scope: hashParams.get('scope'),
77281
- state: hashParams.get('state'),
77282
- obtained_at: Date.now()
77283
- };
77284
-
77285
- // Send tokens to server for storage
77286
- fetch('/oauth/tokens', {
77287
- method: 'POST',
77288
- headers: {
77289
- 'Content-Type': 'application/json'
77290
- },
77291
- body: JSON.stringify(tokens)
77292
- })
77293
- .then(response => {
77294
- if (response.ok) {
77295
- displayStatus('\u2705 Authorization successful!', 'success');
77296
- displayDetails('You can safely close this window.');
77297
- } else {
77298
- return response.json().then(err => {
77299
- throw new Error(err.error || 'Failed to store access token');
77300
- });
77301
- }
77302
- })
77303
- .catch(error => {
77304
- displayStatus('\u274C Error completing authorization', 'error');
77305
- displayDetails(error.message + '. Please try again.');
77306
- });
77307
- }
77308
- // Check for errors in query parameters
77309
- else if (urlParams.has('error')) {
77310
- const error = urlParams.get('error');
77311
- const errorDescription = urlParams.get('error_description');
77312
- displayStatus('\u274C Authorization failed', 'error');
77313
- displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
77314
- }
77315
- // Check for errors in URL fragment (implicit flow errors)
77316
- else if (hashParams.has('error')) {
77317
- const error = hashParams.get('error');
77318
- const errorDescription = hashParams.get('error_description');
77319
- displayStatus('\u274C Authorization failed', 'error');
77320
- displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
77321
- }
77322
- // No authorization code or access token found
77323
- else {
77324
- displayStatus('\u274C No authorization code or access token received', 'error');
77325
- 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';
77326
77431
  }
77327
77432
  </script>
77328
77433
  </body>
@@ -77337,6 +77442,17 @@ var OAuthCallbackServer = class {
77337
77442
  this.setupRoutes();
77338
77443
  }
77339
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
+ });
77340
77456
  this.app.use(import_express.default.json());
77341
77457
  this.app.get("/oauth/callback", (req, res) => {
77342
77458
  logger.oauth("OAuth 2.1 authorization callback page requested");
@@ -77344,46 +77460,47 @@ var OAuthCallbackServer = class {
77344
77460
  });
77345
77461
  this.app.post("/oauth/callback", async (req, res) => {
77346
77462
  try {
77347
- const { code, state, iss } = req.body;
77348
- if (!code) {
77349
- throw new OAuthError2("No authorization code received");
77350
- }
77351
- logger.oauth("OAuth 2.1 authorization code received");
77352
- logger.debug("Authorization code details", "OAUTH", {
77353
- codeLength: code.length,
77354
- state: state || "none",
77355
- issuer: iss || "none"
77356
- });
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");
77357
77470
  this.events.emit("oauth-code-received", { code, state, iss });
77358
- 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
+ });
77359
77475
  } catch (error51) {
77360
- logger.error("Error processing authorization code", "OAUTH", error51);
77361
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
77362
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
77363
- 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 });
77364
77479
  }
77365
77480
  });
77366
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
+ }
77367
77486
  try {
77368
- const tokens = req.body;
77369
- if (!tokens.access_token) {
77370
- throw new OAuthError2("No access token received");
77371
- }
77372
- logger.oauth("Legacy OAuth tokens received (implicit flow)");
77373
- logger.debug("Token details", "OAUTH", {
77374
- tokenType: tokens.token_type,
77375
- expiresIn: tokens.expires_in,
77376
- scope: tokens.scope,
77377
- tokenLength: tokens.access_token.length
77378
- });
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
+ };
77379
77497
  await writeTokens(this.options.serverUrlHash, tokens);
77380
77498
  this.events.emit("oauth-success", tokens);
77381
- res.json({ success: true, message: "Tokens saved successfully" });
77499
+ res.json({ success: true });
77382
77500
  } catch (error51) {
77383
- logger.error("Error processing OAuth tokens", "OAUTH", error51);
77384
- const errorMessage = error51 instanceof Error ? error51.message : "Unknown error";
77385
- this.events.emit("oauth-error", new OAuthError2(errorMessage));
77386
- 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 });
77387
77504
  }
77388
77505
  });
77389
77506
  this.app.get("/oauth/health", (req, res) => {
@@ -77400,21 +77517,33 @@ var OAuthCallbackServer = class {
77400
77517
  }
77401
77518
  );
77402
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
+ }
77403
77526
  async start() {
77404
77527
  return new Promise((resolve, reject) => {
77405
77528
  try {
77406
- this.server = this.app.listen(this.options.port, this.options.host, () => {
77529
+ this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
77530
+ clearTimeout(this.startupTimer);
77531
+ if (error51) {
77532
+ reject(error51);
77533
+ return;
77534
+ }
77407
77535
  logger.oauth(
77408
77536
  `OAuth callback server listening on http://${this.options.host}:${this.options.port}`
77409
77537
  );
77410
77538
  resolve();
77411
77539
  });
77412
77540
  this.server.on("error", (error51) => {
77541
+ clearTimeout(this.startupTimer);
77413
77542
  logger.error("OAuth callback server error", "OAUTH", error51);
77414
77543
  reject(error51);
77415
77544
  });
77416
77545
  if (this.options.timeout) {
77417
- setTimeout(() => {
77546
+ this.startupTimer = setTimeout(() => {
77418
77547
  if (!this.server?.listening) {
77419
77548
  reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
77420
77549
  }
@@ -77426,6 +77555,7 @@ var OAuthCallbackServer = class {
77426
77555
  });
77427
77556
  }
77428
77557
  async stop() {
77558
+ clearTimeout(this.startupTimer);
77429
77559
  if (!this.server) return;
77430
77560
  const server = this.server;
77431
77561
  this.server = null;
@@ -77457,7 +77587,9 @@ var MCPOAuthProvider = class {
77457
77587
  this.authServerMetadata = null;
77458
77588
  this.resourceMetadata = null;
77459
77589
  const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
77460
- 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;
77461
77593
  this.config = {
77462
77594
  serverUrl,
77463
77595
  resource: generateCanonicalResourceURI(serverUrl),
@@ -77516,187 +77648,217 @@ var MCPOAuthProvider = class {
77516
77648
  /**
77517
77649
  * Discover OAuth 2.1 server endpoints using MCP-compliant discovery
77518
77650
  */
77519
- async discoverOAuthEndpoints() {
77520
- try {
77521
- logger.oauth("Starting MCP-compliant OAuth endpoint discovery");
77522
- try {
77523
- this.resourceMetadata = await discoverProtectedResourceMetadata(this.config.resource);
77524
- logger.oauth("Protected resource metadata discovered");
77525
- } catch (error51) {
77526
- logger.warn(
77527
- "Protected resource metadata discovery failed, trying 401 response method",
77528
- "OAUTH"
77529
- );
77530
- await this.discoverVia401Response();
77531
- }
77532
- const authServerUrl = this.getAuthorizationServerUrl();
77533
- if (authServerUrl) {
77534
- this.authServerMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
77535
- logger.oauth("Authorization server metadata discovered");
77536
- this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
77537
- this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
77538
- this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
77539
- } else {
77540
- throw new OAuthError2("Could not determine authorization server URL", "DISCOVERY_FAILED");
77541
- }
77542
- } catch (error51) {
77543
- logger.error("OAuth endpoint discovery failed", "OAUTH", error51);
77544
- throw error51;
77545
- }
77546
- }
77547
- /**
77548
- * Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
77549
- */
77550
- async discoverVia401Response() {
77551
- try {
77552
- logger.oauth("Attempting discovery via 401 response method");
77553
- const customHeaders = getCustomHeaders();
77554
- const response = await proxyFetch(this.config.serverUrl, {
77555
- headers: {
77556
- Accept: "application/json",
77557
- ...customHeaders
77558
- }
77559
- });
77560
- if (response.status === 401) {
77561
- const wwwAuthHeader = response.headers.get("WWW-Authenticate");
77562
- if (wwwAuthHeader) {
77563
- const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
77564
- logger.oauth("WWW-Authenticate header parsed successfully");
77565
- if (authInfo.resource_metadata_url) {
77566
- const metadataResponse = await proxyFetch(authInfo.resource_metadata_url, {
77567
- headers: {
77568
- Accept: "application/json",
77569
- ...customHeaders
77570
- }
77571
- });
77572
- if (metadataResponse.ok) {
77573
- this.resourceMetadata = await metadataResponse.json();
77574
- logger.oauth("Protected resource metadata obtained via 401 response");
77575
- }
77576
- }
77577
- }
77578
- }
77579
- } catch (error51) {
77580
- logger.warn("401 response discovery method failed", "OAUTH", error51);
77581
- }
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
+ ];
77582
77670
  }
77583
- /**
77584
- * Get authorization server URL from discovered metadata
77585
- */
77586
77671
  getAuthorizationServerUrl() {
77587
- if (this.resourceMetadata?.authorization_servers?.length) {
77588
- return this.resourceMetadata.authorization_servers[0];
77589
- }
77590
- try {
77591
- const url2 = new URL(this.config.serverUrl);
77592
- return url2.origin;
77593
- } catch {
77594
- return null;
77595
- }
77672
+ return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
77596
77673
  }
77597
77674
  /**
77598
77675
  * Perform dynamic client registration if needed and supported
77599
77676
  */
77600
77677
  async ensureClientRegistration() {
77678
+ const issuer = this.getAuthorizationServerUrl();
77679
+ const stored = await readClientInfo(this.serverUrlHash);
77601
77680
  if (this.config.clientId) {
77602
- 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 });
77603
77688
  return;
77604
77689
  }
77605
- const storedClientInfo = await readClientInfo(this.serverUrlHash);
77606
- if (typeof storedClientInfo?.client_id === "string" && storedClientInfo.client_id.length > 0) {
77607
- this.config.clientId = storedClientInfo.client_id;
77608
- 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;
77609
77692
  return;
77610
77693
  }
77611
- if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION) {
77694
+ if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
77612
77695
  throw new OAuthError2(
77613
- "No client ID provided and dynamic registration is disabled",
77696
+ "No issuer-bound client registration; configure a client ID or enable registration",
77614
77697
  "NO_CLIENT_ID"
77615
77698
  );
77616
- }
77617
- if (!this.authServerMetadata?.registration_endpoint) {
77699
+ if (!this.authServerMetadata?.registration_endpoint)
77618
77700
  throw new OAuthError2(
77619
- "Dynamic client registration not supported by authorization server",
77701
+ "Authorization server does not support dynamic registration; configure its client ID",
77620
77702
  "NO_DYNAMIC_REGISTRATION"
77621
77703
  );
77622
- }
77623
- try {
77624
- logger.oauth("Performing dynamic client registration");
77625
- const registrationRequest = {
77626
- redirect_uris: [this.config.redirectUri],
77627
- token_endpoint_auth_method: "none",
77628
- // Public client
77629
- grant_types: ["authorization_code"],
77630
- response_types: ["code"],
77631
- client_name: "WordPress MCP Remote",
77632
- scope: this.config.scopes.join(" ")
77633
- };
77634
- const registrationResponse = await registerDynamicClient(
77635
- this.authServerMetadata.registration_endpoint,
77636
- registrationRequest
77637
- );
77638
- const clientInfo = {
77639
- client_id: registrationResponse.client_id,
77640
- client_secret: registrationResponse.client_secret
77641
- };
77642
- await writeClientInfo(this.serverUrlHash, clientInfo);
77643
- this.config.clientId = registrationResponse.client_id;
77644
- logger.oauth("Dynamic client registration completed successfully");
77645
- } catch (error51) {
77646
- logger.error("Dynamic client registration failed", "OAUTH", error51);
77647
- throw error51;
77648
- }
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;
77649
77723
  }
77650
77724
  /**
77651
77725
  * Initiate MCP-compliant OAuth 2.1 authorization flow
77652
77726
  */
77653
- async authorize() {
77654
- if (this.authPromise) {
77655
- logger.oauth("Authorization already in progress, waiting...");
77656
- await this.authPromise;
77657
- return;
77658
- }
77659
- const existingTokens = await this.tokens();
77660
- if (existingTokens) {
77661
- logger.oauth("Already have valid tokens, skipping authorization");
77662
- 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;
77663
77738
  }
77664
- logger.oauth("Starting MCP-compliant OAuth 2.1 authorization flow");
77665
- 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;
77666
77746
  try {
77667
- await this.authPromise;
77668
- logger.oauth("OAuth 2.1 authorization completed successfully");
77747
+ release = await lockfile.lock(file2, { realpath: false });
77669
77748
  } catch (error51) {
77670
- logger.error("OAuth 2.1 authorization failed", "OAUTH", error51);
77671
- 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();
77672
77812
  } finally {
77673
- this.authPromise = null;
77813
+ await release();
77674
77814
  }
77675
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
+ }
77676
77824
  /**
77677
77825
  * Perform the complete OAuth 2.1 authorization flow
77678
77826
  */
77679
77827
  async performAuthorization() {
77680
77828
  try {
77681
- await this.discoverOAuthEndpoints();
77682
- 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;
77683
77844
  const callbackServer = setupWPOAuthCallbackServer(
77684
77845
  {
77685
77846
  port: callbackPort,
77686
77847
  host: this.config.host,
77687
77848
  serverUrlHash: this.serverUrlHash,
77688
- 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
77689
77853
  },
77690
77854
  this.events
77691
77855
  );
77692
77856
  await callbackServer.start();
77693
77857
  logger.oauth("OAuth callback server started");
77694
77858
  try {
77695
- const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
77859
+ const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
77696
77860
  this.config.redirectUri = actualRedirectUri;
77697
77861
  await this.ensureClientRegistration();
77698
- this.currentPKCE = generatePKCE();
77699
- this.currentState = generateSecureState();
77700
77862
  await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
77701
77863
  await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
77702
77864
  const authUrl = buildAuthorizationUrl(
@@ -77706,10 +77868,13 @@ var MCPOAuthProvider = class {
77706
77868
  this.config.scopes,
77707
77869
  this.currentState,
77708
77870
  this.currentPKCE.codeChallenge,
77709
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77871
+ this.resourceIndicator()
77710
77872
  );
77711
77873
  logger.oauth("Built OAuth 2.1 authorization URL");
77712
77874
  logger.debug("Authorization URL", "OAUTH", { url: authUrl });
77875
+ const authorizationCode = this.waitForAuthorizationCode();
77876
+ void authorizationCode.catch(() => {
77877
+ });
77713
77878
  try {
77714
77879
  await open_default(authUrl);
77715
77880
  logger.oauth("Browser opened successfully");
@@ -77720,13 +77885,20 @@ var MCPOAuthProvider = class {
77720
77885
  logger.info(`${authUrl}`);
77721
77886
  logger.info("===============================\n");
77722
77887
  }
77723
- const authCode = await this.waitForAuthorizationCode();
77724
- 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
+ };
77725
77895
  await writeTokens(this.serverUrlHash, tokens);
77726
77896
  logger.oauth("OAuth 2.1 tokens stored successfully");
77727
77897
  return tokens;
77728
77898
  } finally {
77729
77899
  await callbackServer.stop();
77900
+ await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77901
+ await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
77730
77902
  }
77731
77903
  } catch (error51) {
77732
77904
  logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
@@ -77747,12 +77919,16 @@ var MCPOAuthProvider = class {
77747
77919
  this.events.removeAllListeners("oauth-error");
77748
77920
  clearTimeout(timeout);
77749
77921
  };
77750
- this.events.once("oauth-code-received", async ({ code, state }) => {
77922
+ this.events.once("oauth-code-received", async ({ code, state, iss }) => {
77751
77923
  cleanup();
77752
77924
  if (state !== this.currentState) {
77753
77925
  reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
77754
77926
  return;
77755
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
+ }
77756
77932
  logger.oauth("Valid authorization code received");
77757
77933
  resolve(code);
77758
77934
  });
@@ -77778,7 +77954,7 @@ var MCPOAuthProvider = class {
77778
77954
  redirectUri,
77779
77955
  this.config.clientId,
77780
77956
  codeVerifier,
77781
- CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
77957
+ this.resourceIndicator()
77782
77958
  );
77783
77959
  await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
77784
77960
  await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
@@ -77844,6 +78020,7 @@ import * as path5 from "node:path";
77844
78020
  import * as os3 from "node:os";
77845
78021
 
77846
78022
  // src/lib/persistent-oauth-client-provider.ts
78023
+ import { randomBytes } from "node:crypto";
77847
78024
  import { EventEmitter as EventEmitter2 } from "node:events";
77848
78025
  var PersistentWPOAuthClientProvider = class {
77849
78026
  constructor(options) {
@@ -78001,7 +78178,10 @@ var PersistentWPOAuthClientProvider = class {
78001
78178
  */
78002
78179
  async performAuthorization() {
78003
78180
  const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
78181
+ const state = this.generateState();
78004
78182
  const callbackServerOptions = {
78183
+ flow: "implicit",
78184
+ state,
78005
78185
  port: callbackPort,
78006
78186
  host: this.options.host,
78007
78187
  serverUrlHash: this.serverUrlHash,
@@ -78013,12 +78193,14 @@ var PersistentWPOAuthClientProvider = class {
78013
78193
  logger.oauth("Starting callback server...");
78014
78194
  await callbackServer.start();
78015
78195
  logger.oauth("Callback server started successfully");
78016
- const state = this.generateState();
78017
78196
  logger.debug(`Generated state parameter: ${state}`, "OAUTH");
78018
78197
  const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
78019
78198
  logger.oauth(`Built authorization URL: ${authUrl}`);
78020
78199
  logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
78021
78200
  logger.oauth("Attempting to open browser...");
78201
+ const authorization = this.waitForAuthorizationResult();
78202
+ void authorization.catch(() => {
78203
+ });
78022
78204
  try {
78023
78205
  await open_default(authUrl);
78024
78206
  logger.oauth("Browser opened successfully");
@@ -78030,7 +78212,7 @@ var PersistentWPOAuthClientProvider = class {
78030
78212
  logger.info("===============================\n");
78031
78213
  }
78032
78214
  logger.oauth("Waiting for authorization result...");
78033
- const tokens = await this.waitForAuthorizationResult();
78215
+ const tokens = await authorization;
78034
78216
  logger.oauth("Authorization result received");
78035
78217
  logger.oauth("Saving tokens to persistent storage...");
78036
78218
  await this.saveTokens(tokens);
@@ -78083,7 +78265,7 @@ var PersistentWPOAuthClientProvider = class {
78083
78265
  * Generate a secure state parameter
78084
78266
  */
78085
78267
  generateState() {
78086
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
78268
+ return randomBytes(32).toString("base64url");
78087
78269
  }
78088
78270
  /**
78089
78271
  * Wait for authorization result from callback server
@@ -78506,6 +78688,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
78506
78688
  return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
78507
78689
  }
78508
78690
  }
78691
+ function getMCPOAuthProvider() {
78692
+ return mcpOAuthProvider ??= new MCPOAuthProvider({
78693
+ serverUrl: getRequestUrl(),
78694
+ clientId: CONFIG.WP_OAUTH_CLIENT_ID
78695
+ });
78696
+ }
78509
78697
  async function getOAuthTokens() {
78510
78698
  try {
78511
78699
  if (!CONFIG.OAUTH_ENABLED) {
@@ -78513,7 +78701,7 @@ async function getOAuthTokens() {
78513
78701
  return null;
78514
78702
  }
78515
78703
  logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
78516
- const serverUrl = CONFIG.WP_API_URL;
78704
+ const serverUrl = getRequestUrl();
78517
78705
  const serverUrlHash = generateServerUrlHash(serverUrl);
78518
78706
  const existingTokens = await getValidTokens(serverUrlHash);
78519
78707
  if (existingTokens) {
@@ -78524,16 +78712,10 @@ async function getOAuthTokens() {
78524
78712
  logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
78525
78713
  logger.auth("Your browser should open automatically for authentication");
78526
78714
  if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE) {
78527
- if (!mcpOAuthProvider) {
78528
- mcpOAuthProvider = new MCPOAuthProvider({
78529
- serverUrl,
78530
- clientId: CONFIG.WP_OAUTH_CLIENT_ID,
78531
- scopes: getDefaultOAuthScopes()
78532
- });
78533
- }
78534
78715
  logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
78535
- await mcpOAuthProvider.authorize();
78536
- const tokens = await mcpOAuthProvider.tokens();
78716
+ const provider = getMCPOAuthProvider();
78717
+ await provider.authorize();
78718
+ const tokens = await provider.tokens();
78537
78719
  if (tokens) {
78538
78720
  logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
78539
78721
  return tokens;
@@ -78630,7 +78812,7 @@ function updateSessionId(sessionId) {
78630
78812
  globalSessionId = sessionId;
78631
78813
  logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
78632
78814
  }
78633
- async function getWordPressHeaders(requestData) {
78815
+ async function getWordPressHeaders(requestData, options = {}) {
78634
78816
  validateEnvironment();
78635
78817
  let authHeader = "";
78636
78818
  if (CONFIG.JWT_TOKEN) {
@@ -78639,7 +78821,7 @@ async function getWordPressHeaders(requestData) {
78639
78821
  logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
78640
78822
  } else if (CONFIG.OAUTH_ENABLED) {
78641
78823
  logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
78642
- const oauthTokens = await getOAuthTokens();
78824
+ const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
78643
78825
  if (oauthTokens) {
78644
78826
  authHeader = `Bearer ${oauthTokens.access_token}`;
78645
78827
  logger.auth("Using OAuth token authentication for WordPress API");
@@ -78679,7 +78861,7 @@ async function getWordPressHeaders(requestData) {
78679
78861
  if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
78680
78862
  }
78681
78863
  const hasCustomHeaders = Object.keys(customHeaders).length > 0;
78682
- if (!authHeader && !hasCustomHeaders) {
78864
+ if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
78683
78865
  throw new AuthError(
78684
78866
  "No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
78685
78867
  "NO_AUTH_METHOD"