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