@respira/wordpress-mcp-server 8.3.19 → 8.3.21
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/CHANGELOG.md +70 -0
- package/README.md +3 -2
- package/TOOL_CATALOG.md +38 -37
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.d.ts +2 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.d.ts.map +1 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js +91 -0
- package/dist/__tests__/0e6bd33c-redeem-preserves-per-site-settings.test.js.map +1 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.d.ts +30 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.d.ts.map +1 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.js +162 -0
- package/dist/__tests__/139f7353-put-fallback-dedicated-methods.test.js.map +1 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.d.ts +2 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.d.ts.map +1 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js +207 -0
- package/dist/__tests__/44fcf6b5-basic-auth-is-not-a-blocked-verb.test.js.map +1 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.d.ts +2 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.d.ts.map +1 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.js +115 -0
- package/dist/__tests__/e105ed25-report-issue-builder-context.test.js.map +1 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.d.ts +2 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.d.ts.map +1 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.js +83 -0
- package/dist/__tests__/respira-outage-blamed-on-the-site.test.js.map +1 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.d.ts +2 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.d.ts.map +1 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.js +108 -0
- package/dist/__tests__/v2-negotiation-ttl-retry.test.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +59 -4
- package/dist/config.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +51 -4
- package/dist/server.js.map +1 -1
- package/dist/wordpress-client.d.ts +77 -1
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +385 -74
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +2 -2
- package/skills/woo-marketing-campaigns/SKILL.md +128 -0
- package/skills/woo-pricing-promotions/SKILL.md +119 -0
- package/tool-capabilities.json +107 -347
package/dist/wordpress-client.js
CHANGED
|
@@ -286,12 +286,62 @@ const PUT_FALLBACK_CONNECTION_CODES = new Set(['ECONNABORTED', 'ETIMEDOUT', 'ECO
|
|
|
286
286
|
export function isRetryableAsPost(error) {
|
|
287
287
|
if (PUT_FALLBACK_STATUSES.has(error?.response?.status))
|
|
288
288
|
return true;
|
|
289
|
-
if (error
|
|
289
|
+
if (PUT_FALLBACK_STATUSES.has(codedNonWordPressStatus(error)))
|
|
290
290
|
return true;
|
|
291
291
|
if (error?.response)
|
|
292
292
|
return false;
|
|
293
293
|
return PUT_FALLBACK_CONNECTION_CODES.has(String(error?.code || '').toUpperCase());
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* The HTTP status behind a `http_<status>_non_wordpress` coded error, or NaN.
|
|
297
|
+
*
|
|
298
|
+
* `handleError` rebuilds every non-2xx into a fresh `Error` via `codedError`,
|
|
299
|
+
* which does not carry `.response`. So check (1) in `isRetryableAsPost` —
|
|
300
|
+
* `error.response.status` — can only ever fire on an error that has NOT been
|
|
301
|
+
* through the interceptor. Through the real client it never fires, and the
|
|
302
|
+
* hardcoded `error.name === 'http_403_non_wordpress'` check that patched
|
|
303
|
+
* around that covered exactly one of the three statuses in
|
|
304
|
+
* PUT_FALLBACK_STATUSES.
|
|
305
|
+
*
|
|
306
|
+
* That is why ticket 139f7353's host was not rescued by the fallback even
|
|
307
|
+
* where the fallback was already wired in: its edge answers PUT with 405 and
|
|
308
|
+
* an HTML body, which arrives as `http_405_non_wordpress` with no `.response`
|
|
309
|
+
* and no matching name, so the retry was skipped and the tool surfaced the
|
|
310
|
+
* 405. Reading the status back out of the code keeps PUT_FALLBACK_STATUSES
|
|
311
|
+
* the single source of truth instead of growing a second hardcoded list.
|
|
312
|
+
*
|
|
313
|
+
* Deliberately only matches the `_non_wordpress` codes. A 403/404/405 raised
|
|
314
|
+
* BY WordPress arrives with its own WP_Error code stamped on `.name`, and
|
|
315
|
+
* those are real decisions ("you may not edit this", "no such route"), not
|
|
316
|
+
* verb-level routing facts, so they must still not be retried.
|
|
317
|
+
*/
|
|
318
|
+
function codedNonWordPressStatus(error) {
|
|
319
|
+
const match = /^http_(\d{3})_non_wordpress$/.exec(String(error?.name || ''));
|
|
320
|
+
return match ? Number(match[1]) : NaN;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Error codes that mean the failure came from the RESPIRA account service, not
|
|
324
|
+
* from the customer's WordPress install.
|
|
325
|
+
*
|
|
326
|
+
* The plugin cannot validate a dashboard site token or an OAuth access token
|
|
327
|
+
* locally: it POSTs to respira.press/api/mcp/site-token/introspect. When that
|
|
328
|
+
* call fails, the plugin still has to answer the agent with something, and the
|
|
329
|
+
* something is a 5xx carrying one of these codes.
|
|
330
|
+
*
|
|
331
|
+
* `respira_invalid_site_token` is on the list for a reason that looks wrong at
|
|
332
|
+
* first. On plugin builds shipped before this fix, ANY introspection failure —
|
|
333
|
+
* including a Respira-side Supabase outage — came back with that code and the
|
|
334
|
+
* upstream HTTP status adopted verbatim. Those builds are in the field. A 500
|
|
335
|
+
* carrying `respira_invalid_site_token` is therefore a Respira outage, not a
|
|
336
|
+
* bad token: a genuinely invalid token comes back 401/403, which classifies as
|
|
337
|
+
* `auth`, not `server`.
|
|
338
|
+
*/
|
|
339
|
+
const RESPIRA_ACCOUNT_SERVICE_CODES = new Set([
|
|
340
|
+
'respira_site_token_validation_failed',
|
|
341
|
+
'respira_invalid_site_token',
|
|
342
|
+
'introspection_unavailable',
|
|
343
|
+
'introspection_failed',
|
|
344
|
+
]);
|
|
295
345
|
/**
|
|
296
346
|
* Classify a failed v2 status probe.
|
|
297
347
|
*
|
|
@@ -308,45 +358,125 @@ export function classifyV2Failure(error) {
|
|
|
308
358
|
return { kind: 'auth', status, code, detail };
|
|
309
359
|
if (status === 404)
|
|
310
360
|
return { kind: 'missing', status };
|
|
361
|
+
// `code` was being dropped here while the auth branch kept it, so a 5xx that
|
|
362
|
+
// said in plain text "this came from the Respira account service" arrived at
|
|
363
|
+
// describeV2Failure as an anonymous server error and got site-side advice.
|
|
311
364
|
if (Number.isFinite(status) && status > 0)
|
|
312
|
-
return { kind: 'server', status, detail };
|
|
365
|
+
return { kind: 'server', status, code, detail };
|
|
313
366
|
return { kind: 'network', detail: typeof error?.message === 'string' ? error.message : undefined };
|
|
314
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Default: how long a FAILED v2 negotiation stays cached before the next
|
|
370
|
+
* v2-gated call re-probes instead of reusing the cached failure.
|
|
371
|
+
*
|
|
372
|
+
* A SUCCESSFUL negotiation ('v2') is cached for the life of the
|
|
373
|
+
* WordPressClient — respira/v2 does not appear and disappear on a healthy
|
|
374
|
+
* site, and re-probing it on every call would be pure overhead. A FAILURE is
|
|
375
|
+
* different: it can mean "this site really is v1-only" (worth caching, so a
|
|
376
|
+
* genuinely v1-only site isn't re-probed on every single tool call) or "the
|
|
377
|
+
* host had a transient 500 / hiccup" (must NOT stay cached, or every retry
|
|
378
|
+
* for the rest of the session repeats a stale verdict). The client cannot
|
|
379
|
+
* tell those two cases apart from here, so both get this same short TTL
|
|
380
|
+
* rather than the permanent latch that stranded tickets 7dce1a8a and
|
|
381
|
+
* f3310791 for 15+ minutes: every retry returned the identical cached
|
|
382
|
+
* message with zero new HTTP requests and nothing in the site's server logs.
|
|
383
|
+
*
|
|
384
|
+
* Overridable via RESPIRA_V2_NEGOTIATION_TTL_MS (ms). Not a documented
|
|
385
|
+
* customer-facing setting — it exists so tests don't need to sleep for a
|
|
386
|
+
* full production TTL.
|
|
387
|
+
*/
|
|
388
|
+
export const DEFAULT_V2_NEGOTIATION_FAILURE_TTL_MS = 60_000;
|
|
389
|
+
export function v2NegotiationFailureTtlMs(env = process.env) {
|
|
390
|
+
const raw = env.RESPIRA_V2_NEGOTIATION_TTL_MS;
|
|
391
|
+
if (raw === undefined || raw === '')
|
|
392
|
+
return DEFAULT_V2_NEGOTIATION_FAILURE_TTL_MS;
|
|
393
|
+
const parsed = Number(raw);
|
|
394
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_V2_NEGOTIATION_FAILURE_TTL_MS;
|
|
395
|
+
}
|
|
315
396
|
/**
|
|
316
397
|
* Turn a negotiation failure into something the reader can act on.
|
|
317
398
|
*
|
|
318
399
|
* Each branch names what is actually known, and only the `missing` branch
|
|
319
400
|
* keeps the old list of environmental causes, because that list is only ever
|
|
320
401
|
* true for a real 404.
|
|
402
|
+
*
|
|
403
|
+
* @param observedAt When set, this failure is being reported from cache
|
|
404
|
+
* rather than from a probe made during this call — pass the timestamp
|
|
405
|
+
* (`Date.now()` epoch ms) the failure was first observed so the reader isn't
|
|
406
|
+
* told about a request that didn't happen. `null`/`undefined` means the probe
|
|
407
|
+
* ran during this call and the result is fresh.
|
|
321
408
|
*/
|
|
322
|
-
export function describeV2Failure(failure, siteUrl) {
|
|
409
|
+
export function describeV2Failure(failure, siteUrl, observedAt) {
|
|
323
410
|
const probe = `${siteUrl}/wp-json/respira/v2/status`;
|
|
411
|
+
let message;
|
|
324
412
|
if (failure?.kind === 'auth') {
|
|
325
|
-
|
|
326
|
-
(failure.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
`
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
413
|
+
message =
|
|
414
|
+
`respira/v2 rejected this API key (HTTP ${failure.status}${failure.code ? `, ${failure.code}` : ''}).\n\n` +
|
|
415
|
+
(failure.detail ? `The site said: ${failure.detail}\n\n` : '') +
|
|
416
|
+
'The v2 routes exist and are reachable — this is an authentication result, not a missing namespace. ' +
|
|
417
|
+
'Most often the key was deactivated (a lapsed licence disables API keys, and renewing does not ' +
|
|
418
|
+
'currently re-enable them) or it was minted for a different site.\n\n' +
|
|
419
|
+
'To fix: reconnect the site from respira.press/dashboard/sites to mint a fresh key, then restart your MCP client.';
|
|
420
|
+
}
|
|
421
|
+
else if (failure?.kind === 'server' && failure.code && RESPIRA_ACCOUNT_SERVICE_CODES.has(failure.code)) {
|
|
422
|
+
// The customer who triggered this fix read "The site said: Could not query
|
|
423
|
+
// the database for the schema cache. Retrying." plus "Check
|
|
424
|
+
// wp-content/debug.log" and spent hours hunting a fault in his own install.
|
|
425
|
+
// That sentence was PostgREST PGRST002 from the Respira Supabase. Say whose
|
|
426
|
+
// fault it is, and never send the reader to their own logs for it.
|
|
427
|
+
message =
|
|
428
|
+
`respira/v2 returned HTTP ${failure.status} on this site (${failure.code}).\n\n` +
|
|
429
|
+
'This is a Respira account-service problem, not a problem with this WordPress site. ' +
|
|
430
|
+
'The plugin cannot validate a dashboard or OAuth token on its own, so it asks respira.press; ' +
|
|
431
|
+
'respira.press did not answer, and the plugin refused the call rather than guess.\n\n' +
|
|
432
|
+
(failure.detail ? `respira.press reported: ${failure.detail}\n\n` : '') +
|
|
433
|
+
'There is nothing to fix on this site, and nothing about this will appear in its logs. ' +
|
|
434
|
+
'Retry in a minute. If it persists, contact https://respira.press/support with the time and the code above.';
|
|
435
|
+
}
|
|
436
|
+
else if (failure?.kind === 'server') {
|
|
437
|
+
message =
|
|
438
|
+
`respira/v2 returned HTTP ${failure.status} on this site${failure.code ? ` (${failure.code})` : ''}.\n\n` +
|
|
439
|
+
(failure.detail ? `The site said: ${failure.detail}\n\n` : '') +
|
|
440
|
+
`The namespace is registered but the request failed server-side. Check wp-content/debug.log, then open ${probe}.`;
|
|
441
|
+
}
|
|
442
|
+
else if (failure?.kind === 'network') {
|
|
443
|
+
message =
|
|
444
|
+
'Could not reach respira/v2 on this site.\n\n' +
|
|
445
|
+
(failure.detail ? `${failure.detail}\n\n` : '') +
|
|
446
|
+
`This is a connectivity failure, not a plugin problem. Confirm the site is up, then open ${probe}.`;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
// kind === 'missing', or we never got to probe at all.
|
|
450
|
+
message =
|
|
451
|
+
'respira/v2 is not available on this site.\n\n' +
|
|
452
|
+
'Common causes:\n' +
|
|
453
|
+
'• A security plugin (Wordfence, Sucuri, iThemes Security) is blocking the /wp-json/respira/v2/ path — whitelist that path in the plugin settings.\n' +
|
|
454
|
+
'• WordPress permalinks are set to "Plain" — go to Settings → Permalinks and choose any option except Plain.\n' +
|
|
455
|
+
'• A WAF or CDN (e.g. Cloudflare) is blocking requests with the X-Respira-API-Key header — add a WAF exception for /wp-json/respira/.\n' +
|
|
456
|
+
'• The Respira plugin is outdated — update to the latest version from your WordPress admin.\n\n' +
|
|
457
|
+
`To diagnose: open ${probe} in a browser. If you see a 404 or blocked page, one of the above applies.`;
|
|
458
|
+
}
|
|
459
|
+
// Ticket 7dce1a8a / f3310791: a transient 500 latched this client to 'v1'
|
|
460
|
+
// for the rest of the session, and every retry repeated this exact message
|
|
461
|
+
// as if it had just re-checked — the plugin's server logs showed nothing
|
|
462
|
+
// because nothing was ever actually resent. A cached failure now says so.
|
|
463
|
+
if (observedAt) {
|
|
464
|
+
const ageMs = Math.max(0, Date.now() - observedAt);
|
|
465
|
+
message +=
|
|
466
|
+
`\n\n(This is a cached result, not a fresh check: v2 was first seen failing at ` +
|
|
467
|
+
`${new Date(observedAt).toISOString()} (${formatFailureAge(ageMs)} ago). ` +
|
|
468
|
+
'A later call will automatically re-probe once the cache expires.)';
|
|
469
|
+
}
|
|
470
|
+
return message;
|
|
471
|
+
}
|
|
472
|
+
/** Render a cached-failure age for `describeV2Failure`'s honesty note. */
|
|
473
|
+
function formatFailureAge(ms) {
|
|
474
|
+
if (ms < 1000)
|
|
475
|
+
return `${ms}ms`;
|
|
476
|
+
const seconds = ms / 1000;
|
|
477
|
+
if (seconds < 60)
|
|
478
|
+
return `${seconds.toFixed(1)}s`;
|
|
479
|
+
return `${(seconds / 60).toFixed(1)}m`;
|
|
350
480
|
}
|
|
351
481
|
/**
|
|
352
482
|
* Guarantee a delete call answers with SOMETHING an agent can act on.
|
|
@@ -443,6 +573,14 @@ export class WordPressClient {
|
|
|
443
573
|
* of blaming the namespace for what was actually an auth rejection.
|
|
444
574
|
*/
|
|
445
575
|
negotiationFailure = null;
|
|
576
|
+
/**
|
|
577
|
+
* `Date.now()` when `negotiationFailure` was set. Drives the TTL that
|
|
578
|
+
* expires a cached FAILURE (see `v2NegotiationFailureTtlMs`) and lets
|
|
579
|
+
* `describeV2Failure` say when a cached result was first observed instead
|
|
580
|
+
* of reading as a fresh check. Always `null` when `negotiatedApiVersion`
|
|
581
|
+
* is `'v2'` or the client hasn't negotiated yet.
|
|
582
|
+
*/
|
|
583
|
+
negotiationFailureAt = null;
|
|
446
584
|
// Version compatibility check — populated lazily on first API call.
|
|
447
585
|
compatibilityChecked = false;
|
|
448
586
|
compatibilityPromise = null;
|
|
@@ -1022,10 +1160,45 @@ export class WordPressClient {
|
|
|
1022
1160
|
}
|
|
1023
1161
|
/**
|
|
1024
1162
|
* Determine whether site supports respira/v2 and cache the result.
|
|
1163
|
+
*
|
|
1164
|
+
* Wraps `negotiateApiVersion()`, which callers that need to know whether
|
|
1165
|
+
* THIS call made a fresh HTTP request (vs. reused a cached failure) use
|
|
1166
|
+
* directly — see `ensureV2()`.
|
|
1025
1167
|
*/
|
|
1026
1168
|
async getApiVersion() {
|
|
1027
|
-
|
|
1028
|
-
|
|
1169
|
+
return (await this.negotiateApiVersion()).version;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Core negotiation. A successful 'v2' result is cached for the life of the
|
|
1173
|
+
* client. A failure (falls back to 'v1') is cached only for
|
|
1174
|
+
* `v2NegotiationFailureTtlMs()` — once that ages out, the cached failure is
|
|
1175
|
+
* discarded and the next call below re-probes for real, rather than
|
|
1176
|
+
* repeating a verdict from a possibly long-gone transient error. See the
|
|
1177
|
+
* comment on `v2NegotiationFailureTtlMs` for why (tickets 7dce1a8a,
|
|
1178
|
+
* f3310791).
|
|
1179
|
+
*
|
|
1180
|
+
* `freshlyProbed` tells the caller whether THIS invocation actually sent
|
|
1181
|
+
* the HTTP request (true — the result, if a failure, is brand new) or
|
|
1182
|
+
* reused an existing cached failure (false — `negotiationFailureAt` names
|
|
1183
|
+
* when it was first observed). `ensureV2()` uses this so a cached failure
|
|
1184
|
+
* is reported honestly instead of reading like a request that just ran.
|
|
1185
|
+
*/
|
|
1186
|
+
async negotiateApiVersion() {
|
|
1187
|
+
if (this.negotiatedApiVersion === 'v2') {
|
|
1188
|
+
return { version: 'v2', freshlyProbed: false };
|
|
1189
|
+
}
|
|
1190
|
+
if (this.negotiatedApiVersion === 'v1') {
|
|
1191
|
+
const ttl = v2NegotiationFailureTtlMs();
|
|
1192
|
+
const age = this.negotiationFailureAt !== null ? Date.now() - this.negotiationFailureAt : Infinity;
|
|
1193
|
+
if (age < ttl) {
|
|
1194
|
+
return { version: 'v1', freshlyProbed: false };
|
|
1195
|
+
}
|
|
1196
|
+
// The cached FAILURE aged out. Clear the latch so the block below runs
|
|
1197
|
+
// a genuinely fresh probe instead of returning stale data — this is
|
|
1198
|
+
// the fix for the permanent-latch bug: only a SUCCESS stays cached
|
|
1199
|
+
// indefinitely; a failure never does.
|
|
1200
|
+
this.negotiatedApiVersion = null;
|
|
1201
|
+
this.negotiationPromise = null;
|
|
1029
1202
|
}
|
|
1030
1203
|
if (!this.negotiationPromise) {
|
|
1031
1204
|
this.negotiationPromise = (async () => {
|
|
@@ -1046,6 +1219,7 @@ export class WordPressClient {
|
|
|
1046
1219
|
if (response.status >= 200 && response.status < 300) {
|
|
1047
1220
|
this.negotiatedApiVersion = 'v2';
|
|
1048
1221
|
this.negotiationFailure = null;
|
|
1222
|
+
this.negotiationFailureAt = null;
|
|
1049
1223
|
return 'v2';
|
|
1050
1224
|
}
|
|
1051
1225
|
this.negotiationFailure = classifyV2Failure({ response });
|
|
@@ -1056,10 +1230,12 @@ export class WordPressClient {
|
|
|
1056
1230
|
this.negotiationFailure = classifyV2Failure(error);
|
|
1057
1231
|
}
|
|
1058
1232
|
this.negotiatedApiVersion = 'v1';
|
|
1233
|
+
this.negotiationFailureAt = Date.now();
|
|
1059
1234
|
return 'v1';
|
|
1060
1235
|
})();
|
|
1061
1236
|
}
|
|
1062
|
-
|
|
1237
|
+
const version = await this.negotiationPromise;
|
|
1238
|
+
return { version, freshlyProbed: true };
|
|
1063
1239
|
}
|
|
1064
1240
|
/**
|
|
1065
1241
|
* Expose negotiated API version for diagnostics.
|
|
@@ -1071,9 +1247,9 @@ export class WordPressClient {
|
|
|
1071
1247
|
* Ensure connected site supports respira/v2.
|
|
1072
1248
|
*/
|
|
1073
1249
|
async ensureV2() {
|
|
1074
|
-
const version = await this.
|
|
1250
|
+
const { version, freshlyProbed } = await this.negotiateApiVersion();
|
|
1075
1251
|
if (version !== 'v2') {
|
|
1076
|
-
throw new Error(describeV2Failure(this.negotiationFailure, this.siteConfig.url));
|
|
1252
|
+
throw new Error(describeV2Failure(this.negotiationFailure, this.siteConfig.url, freshlyProbed ? null : this.negotiationFailureAt));
|
|
1077
1253
|
}
|
|
1078
1254
|
}
|
|
1079
1255
|
// ---------------------------------------------------------------------------
|
|
@@ -1222,12 +1398,33 @@ export class WordPressClient {
|
|
|
1222
1398
|
* `forceWriteMethod: 'post'` on the site config skips the PUT entirely, for
|
|
1223
1399
|
* hosts already known to drop it (support can set it without a plugin
|
|
1224
1400
|
* change). Ticket e2d884cb.
|
|
1401
|
+
*
|
|
1402
|
+
* Ticket 139f7353: "every PUT write goes through here" was aspirational, not
|
|
1403
|
+
* true. The fallback was wired into the shared callers (callRestV1 /
|
|
1404
|
+
* callRestV2) and the three post-like methods, and the ~25 dedicated methods
|
|
1405
|
+
* that call `client.put()` directly were left on a bare PUT. On a host whose
|
|
1406
|
+
* edge blocks PUT (Kinsta + Cloudflare, reported against
|
|
1407
|
+
* `respira_update_menu_item`) `respira_create_menu_item` worked, because it
|
|
1408
|
+
* is a POST, and `respira_update_menu_item` returned 405, so a menu could be
|
|
1409
|
+
* built and never reordered. Every one of those call sites now routes
|
|
1410
|
+
* through here.
|
|
1411
|
+
*
|
|
1412
|
+
* This is safe for the whole set because the plugin registers all of them
|
|
1413
|
+
* with `WP_REST_Server::EDITABLE`, which is literally `'POST, PUT, PATCH'`:
|
|
1414
|
+
* the fallback POST reaches the identical handler. It is deliberately NOT
|
|
1415
|
+
* extended to DELETE. Those routes are registered `DELETABLE`, so a fallback
|
|
1416
|
+
* POST would return `rest_no_route` — a 404 that reads like "the plugin is
|
|
1417
|
+
* too old" and would be a worse diagnosis than the honest 405. Making delete
|
|
1418
|
+
* survive a blocked verb needs a plugin-side route change, not a connector
|
|
1419
|
+
* change; see the ticket for the enumerated list.
|
|
1225
1420
|
*/
|
|
1226
|
-
async putWithPostFallback(client, url, data) {
|
|
1421
|
+
async putWithPostFallback(client, url, data, config = {}) {
|
|
1227
1422
|
const callId = randomUUID();
|
|
1228
|
-
const
|
|
1423
|
+
const extraHeaders = (config.headers || {});
|
|
1424
|
+
const firstAttempt = { ...config, headers: { ...extraHeaders, 'Idempotency-Key': callId } };
|
|
1229
1425
|
const fallbackAttempt = {
|
|
1230
|
-
|
|
1426
|
+
...config,
|
|
1427
|
+
headers: { ...extraHeaders, 'Idempotency-Key': callId, [ORIGINAL_METHOD_HEADER]: 'PUT' },
|
|
1231
1428
|
};
|
|
1232
1429
|
if (this.siteConfig.forceWriteMethod === 'post') {
|
|
1233
1430
|
// No PUT was sent, so there is no earlier attempt to alias onto: a plain
|
|
@@ -1375,9 +1572,27 @@ export class WordPressClient {
|
|
|
1375
1572
|
const res = await axios.get(probeUrl, {
|
|
1376
1573
|
timeout: 10000,
|
|
1377
1574
|
validateStatus: () => true,
|
|
1378
|
-
|
|
1575
|
+
// Ticket 44fcf6b5. This probe used to send `{ Accept }` and nothing
|
|
1576
|
+
// else. Every other request this client makes carries
|
|
1577
|
+
// `this.defaultHeaders`, which is where the site's configured
|
|
1578
|
+
// `httpAuth` Basic credential lives — so on a site behind server-level
|
|
1579
|
+
// HTTP Basic Auth this was the one request that went out without it.
|
|
1580
|
+
// It came back 401 from the web server, `registered` evaluated false,
|
|
1581
|
+
// and the connector then told the operator, in as many words, that the
|
|
1582
|
+
// Respira REST namespace is not registered and the plugin is
|
|
1583
|
+
// deactivated, deleted or failing to load. The plugin was fine; the
|
|
1584
|
+
// connector had simply forgotten to log in for one request and then
|
|
1585
|
+
// reported the resulting silence as a fact about their site.
|
|
1586
|
+
headers: { ...this.defaultHeaders, Accept: 'application/json' },
|
|
1379
1587
|
});
|
|
1380
1588
|
const body = res.data;
|
|
1589
|
+
// A 401/403 is the web server or WordPress declining to answer the
|
|
1590
|
+
// question, not an answer to it. Returning `false` there is a claim the
|
|
1591
|
+
// probe cannot support: the namespace could be perfectly well
|
|
1592
|
+
// registered behind that challenge. `null` means "could not tell", and
|
|
1593
|
+
// every caller already handles it by staying quiet.
|
|
1594
|
+
if (res.status === 401 || res.status === 403)
|
|
1595
|
+
return null;
|
|
1381
1596
|
// A registered namespace answers with its own index. Anything else,
|
|
1382
1597
|
// including another rest_no_route, means it is not there.
|
|
1383
1598
|
const registered = res.status === 200
|
|
@@ -1404,18 +1619,56 @@ export class WordPressClient {
|
|
|
1404
1619
|
// Handle specific error codes
|
|
1405
1620
|
if (status === 401) {
|
|
1406
1621
|
if (isNonWordPressResponse) {
|
|
1407
|
-
// Likely nginx/Apache HTTP Basic Auth or a CDN/WAF challenge page
|
|
1408
|
-
|
|
1622
|
+
// Likely nginx/Apache HTTP Basic Auth or a CDN/WAF challenge page.
|
|
1623
|
+
//
|
|
1624
|
+
// Ticket 44fcf6b5. Two things were wrong with the old wording, and
|
|
1625
|
+
// both of them are the same mistake: describing a guess as a fact.
|
|
1626
|
+
//
|
|
1627
|
+
// 1. When the challenge said `WWW-Authenticate: Basic`, this told
|
|
1628
|
+
// the operator to add "httpAuth" to their site config — WITHOUT
|
|
1629
|
+
// checking whether httpAuth was already there. On the reported
|
|
1630
|
+
// site it was, and it had been sent on this very request. So the
|
|
1631
|
+
// connector's advice was to do the thing that had already been
|
|
1632
|
+
// done, and the one fact that would have moved the investigation
|
|
1633
|
+
// forward (credentials were supplied and the server rejected
|
|
1634
|
+
// them, or the challenge covers this verb specifically) was the
|
|
1635
|
+
// one fact withheld.
|
|
1636
|
+
// 2. When the challenge did NOT say Basic, this asserted that "a
|
|
1637
|
+
// firewall, CDN, or maintenance mode may be blocking API
|
|
1638
|
+
// access". Nothing in a 401 is evidence of maintenance mode. A
|
|
1639
|
+
// 401 is an authentication challenge; maintenance mode is a 503.
|
|
1640
|
+
// Naming maintenance sent people to look for an outage that was
|
|
1641
|
+
// not happening.
|
|
1642
|
+
//
|
|
1643
|
+
// What this says now is only what the response actually contained:
|
|
1644
|
+
// the verb, the realm if the server named one, and whether this
|
|
1645
|
+
// connector sent a credential.
|
|
1646
|
+
const wwwAuth = String(error.response.headers?.['www-authenticate'] || '');
|
|
1409
1647
|
const isBasicAuth = /basic/i.test(wwwAuth);
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
'
|
|
1417
|
-
|
|
1418
|
-
|
|
1648
|
+
const realmMatch = /realm="?([^",]+)"?/i.exec(wwwAuth);
|
|
1649
|
+
const realm = realmMatch ? realmMatch[1].trim() : null;
|
|
1650
|
+
const method = String(error.config?.method || '').toUpperCase() || 'the request';
|
|
1651
|
+
const credentialSent = !!(this.siteConfig.httpAuth?.username && this.siteConfig.httpAuth?.password);
|
|
1652
|
+
const what = isBasicAuth
|
|
1653
|
+
? `The web server asked for HTTP Basic Authentication${realm ? ` (realm "${realm}")` : ''} on this ${method}. ` +
|
|
1654
|
+
'That is the server password, separate from your Respira API key.'
|
|
1655
|
+
: `The web server refused this ${method} with a 401 and an HTML page. ` +
|
|
1656
|
+
(wwwAuth
|
|
1657
|
+
? `It sent an authentication challenge: ${wwwAuth}.`
|
|
1658
|
+
: 'It sent no WWW-Authenticate header, so it did not say which credential it wants.');
|
|
1659
|
+
const hint = credentialSent
|
|
1660
|
+
? 'httpAuth IS configured for this site and those credentials were sent on this request, so this is not a missing-credential problem. ' +
|
|
1661
|
+
'Either the username/password no longer match what the server expects, or the protected area covers this HTTP method and not the ones that are working. ' +
|
|
1662
|
+
`Run respira_diagnose_connection: it reports the status of GET, OPTIONS and PUT separately, which is what separates "wrong password" from "${method} specifically is challenged". ` +
|
|
1663
|
+
'Re-check the credentials against the server config, and check whether the auth block is scoped by method (Apache <Limit>/<LimitExcept>, an nginx `limit_except` inside the protected location).'
|
|
1664
|
+
: isBasicAuth
|
|
1665
|
+
? 'No httpAuth is configured for this site, so no credential was sent. Add one to your site config:\n\n' +
|
|
1666
|
+
' "httpAuth": { "username": "your-user", "password": "your-pass" }\n\n' +
|
|
1667
|
+
'Or exempt the /wp-json/respira/* path from HTTP Basic Auth in your server config.'
|
|
1668
|
+
: 'No httpAuth is configured for this site, so no credential was sent. If the site prompts for a username and password in a browser, add one to your site config:\n\n' +
|
|
1669
|
+
' "httpAuth": { "username": "your-user", "password": "your-pass" }\n\n' +
|
|
1670
|
+
`Otherwise a CDN or WAF is challenging the request. Open ${this.siteConfig.url}/wp-json/respira/v1/context/site-info in a browser to see which.`;
|
|
1671
|
+
return this.codedError(`Authentication required: ${this.siteConfig.url} returned a ${status} from the web server (not WordPress).\n\n${what}\n\n${hint}`, 'http_401_non_wordpress');
|
|
1419
1672
|
}
|
|
1420
1673
|
const reason = data?.message || data?.code || 'Invalid API key';
|
|
1421
1674
|
const authError = this.codedError(`Authentication failed: ${reason}`, typeof data?.code === 'string' && data.code ? data.code : 'http_401', data);
|
|
@@ -2993,7 +3246,7 @@ export class WordPressClient {
|
|
|
2993
3246
|
const timeout = updates.content && updates.content.length > 50000
|
|
2994
3247
|
? 60000 // 60 seconds for large content
|
|
2995
3248
|
: 30000; // 30 seconds default
|
|
2996
|
-
const response = await this.client
|
|
3249
|
+
const response = await this.putWithPostFallback(this.client, `/builder/${builder}/modules/${pageId}`, {
|
|
2997
3250
|
module_identifier: moduleIdentifier,
|
|
2998
3251
|
updates,
|
|
2999
3252
|
editTarget,
|
|
@@ -3306,6 +3559,12 @@ export class WordPressClient {
|
|
|
3306
3559
|
server: respHeaders['server'] ?? null,
|
|
3307
3560
|
'x-respira-auth-arrived': respHeaders['x-respira-auth-arrived'] ?? null,
|
|
3308
3561
|
link: respHeaders['link'] ?? null,
|
|
3562
|
+
// Ticket 44fcf6b5: the one header that separates "the edge blocks
|
|
3563
|
+
// this verb" from "the server is asking for a password". It was
|
|
3564
|
+
// being read on every response and recorded on none, so a Basic
|
|
3565
|
+
// Auth challenge on OPTIONS/PUT graded out as a method block and
|
|
3566
|
+
// sent the operator into WAF rules that had nothing to do with it.
|
|
3567
|
+
'www-authenticate': respHeaders['www-authenticate'] ?? null,
|
|
3309
3568
|
},
|
|
3310
3569
|
});
|
|
3311
3570
|
}
|
|
@@ -3437,11 +3696,31 @@ export class WordPressClient {
|
|
|
3437
3696
|
// An HTML 4xx/5xx is something in front of WordPress answering instead.
|
|
3438
3697
|
const putReachedWordPress = !!putProbe && typeof putProbe.status === 'number' && putProbe.looks_like_html === false;
|
|
3439
3698
|
const putBlockedAtEdge = !!putProbe && typeof putProbe.status === 'number' && putProbe.status >= 400 && !putReachedWordPress;
|
|
3699
|
+
// Ticket 44fcf6b5. A 401/407 is not a method block, it is the server
|
|
3700
|
+
// asking for a credential, and the two need opposite fixes. Grading an
|
|
3701
|
+
// authentication challenge as "the edge is blocking non-GET methods"
|
|
3702
|
+
// pointed a customer at Cloudflare and Wordfence rules on a site whose
|
|
3703
|
+
// only gate was an .htaccess password, and the recommendation text never
|
|
3704
|
+
// mentioned Basic Auth at all. `www-authenticate` on the response is the
|
|
3705
|
+
// discriminator; the status alone is not, because some edges challenge
|
|
3706
|
+
// with 403 and some WAFs reject with 401.
|
|
3707
|
+
const authChallenged = (p) => !!p &&
|
|
3708
|
+
typeof p.status === 'number' &&
|
|
3709
|
+
(p.status === 401 || p.status === 407 || !!p.headers?.['www-authenticate']);
|
|
3710
|
+
const putAuthChallenged = authChallenged(putProbe);
|
|
3711
|
+
const optionsAuthChallenged = authChallenged(optionsProbe);
|
|
3712
|
+
const httpAuthConfigured = !!(this.siteConfig.httpAuth?.username && this.siteConfig.httpAuth?.password);
|
|
3440
3713
|
const writeMethodVerdict = (() => {
|
|
3441
3714
|
// If plain GET is not healthy, nothing can be concluded about writes
|
|
3442
3715
|
// specifically — the whole connection is the problem.
|
|
3443
3716
|
if (!getOk)
|
|
3444
3717
|
return 'unknown';
|
|
3718
|
+
// Checked BEFORE `blocked`: a 401 satisfies putBlockedAtEdge too, and
|
|
3719
|
+
// the old ordering meant every auth challenge was reported as a method
|
|
3720
|
+
// block. The verb never reached WordPress, so this diagnostic cannot
|
|
3721
|
+
// say whether the verb itself is allowed — only that it was challenged.
|
|
3722
|
+
if (putAuthChallenged || optionsAuthChallenged)
|
|
3723
|
+
return 'auth_challenge';
|
|
3445
3724
|
if (optionsBlocked || putBlockedAtEdge)
|
|
3446
3725
|
return 'blocked';
|
|
3447
3726
|
if (putDropped || optionsDropped)
|
|
@@ -3452,13 +3731,36 @@ export class WordPressClient {
|
|
|
3452
3731
|
})();
|
|
3453
3732
|
// Kept as a tri-state boolean rather than a string so an existing truthiness
|
|
3454
3733
|
// check still means "proven blocked" and never accidentally fires on the
|
|
3455
|
-
// unknown case. null = could not tell
|
|
3734
|
+
// unknown case. null = could not tell — which is the honest answer for an
|
|
3735
|
+
// auth challenge as well as for a dropped probe.
|
|
3456
3736
|
const writeMethodBlocked = writeMethodVerdict === 'blocked' ? true : writeMethodVerdict === 'not_blocked' ? false : null;
|
|
3457
3737
|
const forceWriteMethodHint = ' Unblock this connection immediately without touching the host: add ' +
|
|
3458
3738
|
`"forceWriteMethod": "post" to the site object in ${CONFIG_FILE}. The Respira plugin ` +
|
|
3459
3739
|
'registers the same handler on PUT and POST for every updatable resource, so POST does ' +
|
|
3460
3740
|
'identical work and the connector stops sending PUT at all. Fixing the host is still the ' +
|
|
3461
3741
|
'better end state, this just stops the bleeding.';
|
|
3742
|
+
if (writeMethodVerdict === 'auth_challenge') {
|
|
3743
|
+
const challengedProbe = putAuthChallenged ? putProbe : optionsProbe;
|
|
3744
|
+
const verb = putAuthChallenged ? 'PUT' : 'OPTIONS';
|
|
3745
|
+
const challenge = String(challengedProbe?.headers?.['www-authenticate'] || '');
|
|
3746
|
+
const scheme = /basic/i.test(challenge)
|
|
3747
|
+
? 'HTTP Basic Auth'
|
|
3748
|
+
: challenge
|
|
3749
|
+
? `an authentication challenge (${challenge})`
|
|
3750
|
+
: 'an authentication challenge with no WWW-Authenticate header';
|
|
3751
|
+
recommendations.push(`${verb} /wp-json/respira/v1/ping returned ${challengedProbe?.status} with ${scheme} while GET returned 2xx. ` +
|
|
3752
|
+
'This is an authentication challenge, not a blocked HTTP verb: the request never reached WordPress, so nothing here ' +
|
|
3753
|
+
'proves the verb is allowed OR disallowed, and no WAF or CDN method rule is implicated. ' +
|
|
3754
|
+
(httpAuthConfigured
|
|
3755
|
+
? 'httpAuth IS configured for this site and was sent on this probe, so the credential is either wrong now or does not cover this method. ' +
|
|
3756
|
+
'Check the username and password against the server, and check whether the protected block is scoped by method ' +
|
|
3757
|
+
'(Apache <Limit>/<LimitExcept> inside the auth block, or an nginx `limit_except` inside the protected location) — ' +
|
|
3758
|
+
'a method-scoped auth block is exactly what makes GET work and everything else fail.'
|
|
3759
|
+
: 'No httpAuth is configured for this site, so no credential was sent. Add ' +
|
|
3760
|
+
`"httpAuth": { "username": "...", "password": "..." } to the site object in ${CONFIG_FILE}, ` +
|
|
3761
|
+
'or exempt /wp-json/respira/* from the password in the server config.') +
|
|
3762
|
+
' Do not set "forceWriteMethod": "post" for this: POST is challenged the same way, so it would change nothing.');
|
|
3763
|
+
}
|
|
3462
3764
|
if (writeMethodVerdict === 'unknown' && (putDropped || optionsDropped)) {
|
|
3463
3765
|
const droppedLabel = putDropped ? 'PUT' : 'OPTIONS';
|
|
3464
3766
|
const droppedCode = String((putDropped ? putProbe : optionsProbe)?.code || 'no response');
|
|
@@ -3532,14 +3834,23 @@ export class WordPressClient {
|
|
|
3532
3834
|
// `write_method_verdict` for the named state; never treat null as false.
|
|
3533
3835
|
write_method_blocked: writeMethodBlocked,
|
|
3534
3836
|
write_method_verdict: writeMethodVerdict,
|
|
3837
|
+
// Ticket 44fcf6b5: whether a credential was configured and sent is part
|
|
3838
|
+
// of the evidence, not background knowledge the reader is assumed to
|
|
3839
|
+
// have. Without it, "401 on PUT" and "401 on PUT with the password
|
|
3840
|
+
// supplied" read identically, and they mean different things.
|
|
3841
|
+
http_auth_configured: httpAuthConfigured,
|
|
3535
3842
|
write_method_probe: {
|
|
3536
3843
|
options_status: optionsProbe?.status ?? null,
|
|
3537
3844
|
options_dropped: optionsDropped,
|
|
3845
|
+
options_auth_challenged: optionsAuthChallenged,
|
|
3538
3846
|
put_status: putProbe?.status ?? null,
|
|
3539
3847
|
put_dropped: putDropped,
|
|
3848
|
+
put_auth_challenged: putAuthChallenged,
|
|
3540
3849
|
put_reached_wordpress: putReachedWordPress,
|
|
3541
3850
|
put_error: putProbe?.error ?? null,
|
|
3542
3851
|
put_error_code: putProbe?.code ?? null,
|
|
3852
|
+
put_www_authenticate: putProbe?.headers?.['www-authenticate'] ?? null,
|
|
3853
|
+
options_www_authenticate: optionsProbe?.headers?.['www-authenticate'] ?? null,
|
|
3543
3854
|
},
|
|
3544
3855
|
target_post_id: opts.post_id ?? null,
|
|
3545
3856
|
recommendations,
|
|
@@ -3728,7 +4039,7 @@ export class WordPressClient {
|
|
|
3728
4039
|
return response.data;
|
|
3729
4040
|
}
|
|
3730
4041
|
async updateUser(id, data) {
|
|
3731
|
-
const response = await this.client
|
|
4042
|
+
const response = await this.putWithPostFallback(this.client, `/users/${id}`, data);
|
|
3732
4043
|
return response.data;
|
|
3733
4044
|
}
|
|
3734
4045
|
async deleteUser(id, reassign, approval_token) {
|
|
@@ -3751,7 +4062,7 @@ export class WordPressClient {
|
|
|
3751
4062
|
return response.data;
|
|
3752
4063
|
}
|
|
3753
4064
|
async updateComment(id, data) {
|
|
3754
|
-
const response = await this.client
|
|
4065
|
+
const response = await this.putWithPostFallback(this.client, `/comments/${id}`, data);
|
|
3755
4066
|
return response.data;
|
|
3756
4067
|
}
|
|
3757
4068
|
async deleteComment(id, approvalToken) {
|
|
@@ -3782,7 +4093,7 @@ export class WordPressClient {
|
|
|
3782
4093
|
return response.data;
|
|
3783
4094
|
}
|
|
3784
4095
|
async updateTerm(taxonomy, id, data) {
|
|
3785
|
-
const response = await this.client
|
|
4096
|
+
const response = await this.putWithPostFallback(this.client, `/taxonomies/${taxonomy}/terms/${id}`, data);
|
|
3786
4097
|
return response.data;
|
|
3787
4098
|
}
|
|
3788
4099
|
async deleteTerm(taxonomy, id, approvalToken) {
|
|
@@ -3945,11 +4256,11 @@ export class WordPressClient {
|
|
|
3945
4256
|
return response.data;
|
|
3946
4257
|
}
|
|
3947
4258
|
async updateMedia(id, data) {
|
|
3948
|
-
const response = await this.client
|
|
4259
|
+
const response = await this.putWithPostFallback(this.client, `/media/${id}`, data);
|
|
3949
4260
|
return response.data.media || response.data;
|
|
3950
4261
|
}
|
|
3951
4262
|
async updateMediaBatch(items) {
|
|
3952
|
-
const response = await this.client
|
|
4263
|
+
const response = await this.putWithPostFallback(this.client, '/media/batch', { items });
|
|
3953
4264
|
return response.data;
|
|
3954
4265
|
}
|
|
3955
4266
|
/**
|
|
@@ -3995,7 +4306,7 @@ export class WordPressClient {
|
|
|
3995
4306
|
return response.data;
|
|
3996
4307
|
}
|
|
3997
4308
|
async updateMenu(id, data) {
|
|
3998
|
-
const response = await this.client
|
|
4309
|
+
const response = await this.putWithPostFallback(this.client, `/menus/${id}`, data);
|
|
3999
4310
|
return response.data;
|
|
4000
4311
|
}
|
|
4001
4312
|
async deleteMenu(id, approval_token) {
|
|
@@ -4010,7 +4321,7 @@ export class WordPressClient {
|
|
|
4010
4321
|
return response.data;
|
|
4011
4322
|
}
|
|
4012
4323
|
async assignMenuToLocation(location, menuId) {
|
|
4013
|
-
const response = await this.client
|
|
4324
|
+
const response = await this.putWithPostFallback(this.client, `/menus/locations/${location}`, { menu_id: menuId });
|
|
4014
4325
|
return response.data;
|
|
4015
4326
|
}
|
|
4016
4327
|
// Menu Items
|
|
@@ -4027,7 +4338,7 @@ export class WordPressClient {
|
|
|
4027
4338
|
return response.data;
|
|
4028
4339
|
}
|
|
4029
4340
|
async updateMenuItem(itemId, data) {
|
|
4030
|
-
const response = await this.client
|
|
4341
|
+
const response = await this.putWithPostFallback(this.client, `/menus/items/${itemId}`, data);
|
|
4031
4342
|
return response.data;
|
|
4032
4343
|
}
|
|
4033
4344
|
async deleteMenuItem(itemId) {
|
|
@@ -4048,7 +4359,7 @@ export class WordPressClient {
|
|
|
4048
4359
|
return response.data;
|
|
4049
4360
|
}
|
|
4050
4361
|
async woocommerceUpdateProduct(id, data) {
|
|
4051
|
-
const response = await this.client
|
|
4362
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/products/${id}`, data);
|
|
4052
4363
|
return response.data;
|
|
4053
4364
|
}
|
|
4054
4365
|
async woocommerceDuplicateProduct(id) {
|
|
@@ -4068,7 +4379,7 @@ export class WordPressClient {
|
|
|
4068
4379
|
return response.data;
|
|
4069
4380
|
}
|
|
4070
4381
|
async woocommerceUpdateCategory(id, data) {
|
|
4071
|
-
const response = await this.client
|
|
4382
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/categories/${id}`, data);
|
|
4072
4383
|
return response.data;
|
|
4073
4384
|
}
|
|
4074
4385
|
async woocommerceDeleteCategory(id) {
|
|
@@ -4088,7 +4399,7 @@ export class WordPressClient {
|
|
|
4088
4399
|
return response.data;
|
|
4089
4400
|
}
|
|
4090
4401
|
async woocommerceUpdateTag(id, data) {
|
|
4091
|
-
const response = await this.client
|
|
4402
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/tags/${id}`, data);
|
|
4092
4403
|
return response.data;
|
|
4093
4404
|
}
|
|
4094
4405
|
async woocommerceDeleteTag(id) {
|
|
@@ -4104,7 +4415,7 @@ export class WordPressClient {
|
|
|
4104
4415
|
return response.data;
|
|
4105
4416
|
}
|
|
4106
4417
|
async woocommerceUpdateOrderStatus(id, status) {
|
|
4107
|
-
const response = await this.client
|
|
4418
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/orders/${id}/status`, { status });
|
|
4108
4419
|
return response.data;
|
|
4109
4420
|
}
|
|
4110
4421
|
async woocommerceGetStockStatus() {
|
|
@@ -4112,7 +4423,7 @@ export class WordPressClient {
|
|
|
4112
4423
|
return response.data;
|
|
4113
4424
|
}
|
|
4114
4425
|
async woocommerceUpdateStock(id, data) {
|
|
4115
|
-
const response = await this.client
|
|
4426
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/products/${id}/stock`, data);
|
|
4116
4427
|
return response.data;
|
|
4117
4428
|
}
|
|
4118
4429
|
async woocommerceSalesReport(params) {
|
|
@@ -4133,7 +4444,7 @@ export class WordPressClient {
|
|
|
4133
4444
|
return response.data;
|
|
4134
4445
|
}
|
|
4135
4446
|
async woocommerceUpdateBrand(id, data) {
|
|
4136
|
-
const response = await this.client
|
|
4447
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/brands/${id}`, data);
|
|
4137
4448
|
return response.data;
|
|
4138
4449
|
}
|
|
4139
4450
|
async woocommerceDeleteBrand(id) {
|
|
@@ -4231,7 +4542,7 @@ export class WordPressClient {
|
|
|
4231
4542
|
return response.data;
|
|
4232
4543
|
}
|
|
4233
4544
|
async woocommerceSetVariationGallery(id, data) {
|
|
4234
|
-
const response = await this.client
|
|
4545
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/variations/${id}/gallery`, data);
|
|
4235
4546
|
return response.data;
|
|
4236
4547
|
}
|
|
4237
4548
|
// WooCommerce Add-on v3.0 — coupons and customers
|
|
@@ -4244,7 +4555,7 @@ export class WordPressClient {
|
|
|
4244
4555
|
return response.data;
|
|
4245
4556
|
}
|
|
4246
4557
|
async woocommerceUpdateCoupon(id, data) {
|
|
4247
|
-
const response = await this.client
|
|
4558
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/coupons/${id}`, data);
|
|
4248
4559
|
return response.data;
|
|
4249
4560
|
}
|
|
4250
4561
|
async woocommerceCreateCustomer(data) {
|
|
@@ -4252,7 +4563,7 @@ export class WordPressClient {
|
|
|
4252
4563
|
return response.data;
|
|
4253
4564
|
}
|
|
4254
4565
|
async woocommerceUpdateCustomer(id, data) {
|
|
4255
|
-
const response = await this.client
|
|
4566
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/customers/${id}`, data);
|
|
4256
4567
|
return response.data;
|
|
4257
4568
|
}
|
|
4258
4569
|
// WooCommerce Add-on v3.0 — order notes and refunds
|
|
@@ -4278,11 +4589,11 @@ export class WordPressClient {
|
|
|
4278
4589
|
return response.data;
|
|
4279
4590
|
}
|
|
4280
4591
|
async woocommerceUpdateSubscriptionStatus(id, data) {
|
|
4281
|
-
const response = await this.client
|
|
4592
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/subscriptions/${id}/status`, data || {});
|
|
4282
4593
|
return response.data;
|
|
4283
4594
|
}
|
|
4284
4595
|
async woocommerceUpdateSubscriptionDates(id, data) {
|
|
4285
|
-
const response = await this.client
|
|
4596
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/subscriptions/${id}/dates`, data || {});
|
|
4286
4597
|
return response.data;
|
|
4287
4598
|
}
|
|
4288
4599
|
async woocommerceAddSubscriptionNote(id, data) {
|
|
@@ -4298,11 +4609,11 @@ export class WordPressClient {
|
|
|
4298
4609
|
return response.data;
|
|
4299
4610
|
}
|
|
4300
4611
|
async woocommerceUpdateBookingStatus(id, data) {
|
|
4301
|
-
const response = await this.client
|
|
4612
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/bookings/${id}/status`, data || {});
|
|
4302
4613
|
return response.data;
|
|
4303
4614
|
}
|
|
4304
4615
|
async woocommerceRescheduleBooking(id, data) {
|
|
4305
|
-
const response = await this.client
|
|
4616
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/bookings/${id}/reschedule`, data || {});
|
|
4306
4617
|
return response.data;
|
|
4307
4618
|
}
|
|
4308
4619
|
async woocommerceListMembershipPlans(params) {
|
|
@@ -4318,11 +4629,11 @@ export class WordPressClient {
|
|
|
4318
4629
|
return response.data;
|
|
4319
4630
|
}
|
|
4320
4631
|
async woocommerceUpdateMembershipStatus(id, data) {
|
|
4321
|
-
const response = await this.client
|
|
4632
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/memberships/${id}/status`, data || {});
|
|
4322
4633
|
return response.data;
|
|
4323
4634
|
}
|
|
4324
4635
|
async woocommerceSetMembershipEndDate(id, data) {
|
|
4325
|
-
const response = await this.client
|
|
4636
|
+
const response = await this.putWithPostFallback(this.client, `/woocommerce/memberships/${id}/end-date`, data || {});
|
|
4326
4637
|
return response.data;
|
|
4327
4638
|
}
|
|
4328
4639
|
// WooCommerce Add-on v3.1 — assisted checkout
|
|
@@ -4535,7 +4846,7 @@ export class WordPressClient {
|
|
|
4535
4846
|
const { method, path, body } = resolver(args);
|
|
4536
4847
|
const req = method === 'GET' ? this.client.get(path)
|
|
4537
4848
|
: method === 'POST' ? this.client.post(path, body || {})
|
|
4538
|
-
: method === 'PUT' ? this.client
|
|
4849
|
+
: method === 'PUT' ? this.putWithPostFallback(this.client, path, body || {})
|
|
4539
4850
|
: this.client.request({ method: 'DELETE', url: path, data: body || {} });
|
|
4540
4851
|
const response = await req;
|
|
4541
4852
|
return response.data;
|