@respira/wordpress-mcp-server 7.5.1 → 7.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/__tests__/delete-media-approval-token.test.d.ts +21 -0
- package/dist/__tests__/delete-media-approval-token.test.d.ts.map +1 -0
- package/dist/__tests__/delete-media-approval-token.test.js +126 -0
- package/dist/__tests__/delete-media-approval-token.test.js.map +1 -0
- package/dist/__tests__/upload-media-path-detection.test.d.ts +2 -0
- package/dist/__tests__/upload-media-path-detection.test.d.ts.map +1 -0
- package/dist/__tests__/upload-media-path-detection.test.js +70 -0
- package/dist/__tests__/upload-media-path-detection.test.js.map +1 -0
- package/dist/__tests__/usage-emitter-privacy.test.d.ts +2 -0
- package/dist/__tests__/usage-emitter-privacy.test.d.ts.map +1 -0
- package/dist/__tests__/usage-emitter-privacy.test.js +16 -0
- package/dist/__tests__/usage-emitter-privacy.test.js.map +1 -0
- package/dist/__tests__/write-retry-safety.test.d.ts +2 -0
- package/dist/__tests__/write-retry-safety.test.d.ts.map +1 -0
- package/dist/__tests__/write-retry-safety.test.js +36 -0
- package/dist/__tests__/write-retry-safety.test.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +36 -3
- package/dist/server.js.map +1 -1
- package/dist/usage-emitter.d.ts +12 -6
- package/dist/usage-emitter.d.ts.map +1 -1
- package/dist/usage-emitter.js +89 -31
- package/dist/usage-emitter.js.map +1 -1
- package/dist/wordpress-client.d.ts +64 -1
- package/dist/wordpress-client.d.ts.map +1 -1
- package/dist/wordpress-client.js +153 -13
- package/dist/wordpress-client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,22 @@ Other WordPress MCP servers wrap the REST API. They can create posts and pages,
|
|
|
35
35
|
|
|
36
36
|
Respira includes a WordPress plugin that gives AI **native access** to 16 page builders, plus element-level precision, full page creation from structure, HTML-to-builder conversion, storefront design intelligence, stock image search, and bulk operations across hundreds of pages.
|
|
37
37
|
|
|
38
|
+
### New in v7.5.2: reconnect-safe writes and drop diagnostics
|
|
39
|
+
|
|
40
|
+
- **Writes carry a stable idempotency key.** The connector automatically retries transient reads. It retries a write only when WordPress explicitly confirms replay support; an ambiguous timeout/reset is reported as `respira_write_outcome_unknown` so the agent verifies the target before trying again.
|
|
41
|
+
- **Telemetry queues and retries independently of tool execution.** A dashboard or network interruption cannot block a WordPress call, and failed metadata batches are retained with bounded exponential backoff.
|
|
42
|
+
- **Local API keys stay local.** With Respira for WordPress 7.5.46+, a local `respira_*` key is exchanged inside WordPress for a revocable, site-scoped telemetry bearer. The central service never receives that local key.
|
|
43
|
+
- **The quality dashboard can separate exact retries from editing sweeps.** It records stable session/call identity, transport, incomplete calls, connector/plugin versions, total vs tool time, and privacy-safe stage durations. Target comparison uses a secret HMAC; arguments, results, prompts, and WordPress content are never sent.
|
|
44
|
+
- **Native URL-connected clients gain timeout-safe writes.** Plugin 7.5.46 adds `_respira_async: true`, `respira_get_job_status`, stable `Mcp-Session-Id` responses, replay-safe `_respira_call_id`, and a native `respira_diagnose_connection` tool. The native endpoint is stateless HTTP—independent POST requests, not a persistent “write channel.”
|
|
45
|
+
|
|
46
|
+
### Also shipped since the README's previous v7.1 refresh
|
|
47
|
+
|
|
48
|
+
- **v7.2:** cascade session rollback; dropped-styling/no-op guidance; page-parent schemas; Oxygen 6 build guidance; completed live-edit confirmation parameters; bounded heavy-write/docs timeouts; and route-aware capability negotiation that hides tools an older plugin cannot execute.
|
|
49
|
+
- **v7.3:** the WooCommerce add-on expanded from 21 to 56 tools across catalog, pricing, stock, storefront, brands, variations/attributes, coupons, customers, refunds, and AI-readiness.
|
|
50
|
+
- **v7.4:** Agent-Ready Commerce expanded WooCommerce to 79 tools with feeds + store `llms.txt`, readiness fixes, signed cart links/attribution, and Subscriptions, Bookings, and Memberships writes. v7.4.1 then made newly connected dashboard sites self-heal into a running connector session.
|
|
51
|
+
- **v7.5.0:** eight STAGGS configurator tools brought the WooCommerce total to 87.
|
|
52
|
+
- **v7.5.1:** fixed normalized ACF tool routing and preserved dashboard-token 401 machine codes—the two fixes shipped immediately before this reliability release.
|
|
53
|
+
|
|
38
54
|
### New in v7.1 "Roots"
|
|
39
55
|
|
|
40
56
|
The biggest release since launch.
|
|
@@ -43,7 +59,7 @@ The biggest release since launch.
|
|
|
43
59
|
- **Make a page responsive in one call** on Bricks, Elementor, Divi 4, Beaver Builder, and WPBakery.
|
|
44
60
|
- **Playbooks.** Teach an agent a multi-step workflow once and rerun it as a single callable WordPress Ability. Five tools (create / list / get / update / delete), with static cycle detection and a runtime depth cap.
|
|
45
61
|
- **Agent-creatable Custom Post Types, taxonomies, and ACF field groups** (twelve tools), stored in options and registered on `init`. No PHP files, server-side slug allowlist, conservative deletes that report orphans.
|
|
46
|
-
- **Native in-WP MCP endpoint.** Point a client straight at `your-site
|
|
62
|
+
- **Native in-WP MCP endpoint.** Point a client straight at `your-site/wp-json/respira/v1/mcp` (Streamable HTTP JSON-RPC; `?rest_route=` fallback supported) with an API key, no npx process required. One-click connect-by-link mints and installs the key for you.
|
|
47
63
|
- **System certificate-store trust.** The connector trusts the OS certificate store at startup (verification stays on) with a bundled Mozilla CA fallback, so valid public certs stop being rejected on Claude Desktop's bundled Node.
|
|
48
64
|
- **Bricks design-token import + section presets**, the **Elementor 4 atomic write path** (partial: 20 atomic element types with `$$type` envelopes, per-breakpoint + per-state styling, and a clear 422 on unsupported types instead of a silent drop), and **`respira_generate_activity_report`** for client-ready reports.
|
|
49
65
|
|
|
@@ -61,7 +77,7 @@ The triage rule, baked into the handshake instructions: tool error → apply hin
|
|
|
61
77
|
|
|
62
78
|
### v6.16.0: usage telemetry ships by default
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
v6.16 introduced default per-tool telemetry so customers did not have to configure an OTEL bearer. As of v7.5.2 with plugin 7.5.46+, local WordPress API keys are never sent to the central ingest: the connector asks WordPress for a revocable, site-scoped telemetry token instead. Privacy remains metadata-only—no prompts, raw tool arguments, tool results, or WordPress content. `RESPIRA_USAGE_OPT_OUT=1` disables the entire stream.
|
|
65
81
|
|
|
66
82
|
### v6.3 — MCP Protocol Compliance
|
|
67
83
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket 927aa7ed: "delete_media approval flow never
|
|
3
|
+
* completes: token rejected in a loop".
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `WordPressClient.deleteMedia()` only ever forwarded `id` to the
|
|
6
|
+
* WP REST endpoint (`DELETE /media/{id}`). The MCP tool schema for
|
|
7
|
+
* `wordpress_delete_media` advertises `approval_token` and `force` params and
|
|
8
|
+
* the server.ts dispatcher received them from the caller, but the client
|
|
9
|
+
* method silently dropped both on the floor before the request ever went
|
|
10
|
+
* out. The WP-side approval gate (Respira_Tool_Governance) never saw a token
|
|
11
|
+
* on the "confirm" call, so it always treated it as a fresh request and
|
|
12
|
+
* minted a brand-new token — an infinite loop, indistinguishable from the
|
|
13
|
+
* outside from a broken approval mechanism, even though the mechanism itself
|
|
14
|
+
* (proven by delete_page working) was fine.
|
|
15
|
+
*
|
|
16
|
+
* Uses the same axios mock-adapter pattern as rest-route-fallback.test.ts.
|
|
17
|
+
*
|
|
18
|
+
* @since 7.5.x (ticket 927aa7ed)
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=delete-media-approval-token.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-media-approval-token.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/delete-media-approval-token.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for ticket 927aa7ed: "delete_media approval flow never
|
|
3
|
+
* completes: token rejected in a loop".
|
|
4
|
+
*
|
|
5
|
+
* Root cause: `WordPressClient.deleteMedia()` only ever forwarded `id` to the
|
|
6
|
+
* WP REST endpoint (`DELETE /media/{id}`). The MCP tool schema for
|
|
7
|
+
* `wordpress_delete_media` advertises `approval_token` and `force` params and
|
|
8
|
+
* the server.ts dispatcher received them from the caller, but the client
|
|
9
|
+
* method silently dropped both on the floor before the request ever went
|
|
10
|
+
* out. The WP-side approval gate (Respira_Tool_Governance) never saw a token
|
|
11
|
+
* on the "confirm" call, so it always treated it as a fresh request and
|
|
12
|
+
* minted a brand-new token — an infinite loop, indistinguishable from the
|
|
13
|
+
* outside from a broken approval mechanism, even though the mechanism itself
|
|
14
|
+
* (proven by delete_page working) was fine.
|
|
15
|
+
*
|
|
16
|
+
* Uses the same axios mock-adapter pattern as rest-route-fallback.test.ts.
|
|
17
|
+
*
|
|
18
|
+
* @since 7.5.x (ticket 927aa7ed)
|
|
19
|
+
*/
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import axios from 'axios';
|
|
23
|
+
import { WordPressClient } from '../wordpress-client.js';
|
|
24
|
+
const callLog = [];
|
|
25
|
+
let currentHandler = null;
|
|
26
|
+
const originalAdapter = axios.defaults.adapter;
|
|
27
|
+
function installMockAdapter() {
|
|
28
|
+
callLog.length = 0;
|
|
29
|
+
axios.defaults.adapter = ((config) => {
|
|
30
|
+
callLog.push({
|
|
31
|
+
method: (config.method || 'get').toLowerCase(),
|
|
32
|
+
url: config.url,
|
|
33
|
+
params: config.params,
|
|
34
|
+
});
|
|
35
|
+
if (!currentHandler) {
|
|
36
|
+
return Promise.reject(new Error('no mock handler installed'));
|
|
37
|
+
}
|
|
38
|
+
const result = currentHandler(config);
|
|
39
|
+
return Promise.resolve(result).then((r) => ({
|
|
40
|
+
status: r.status,
|
|
41
|
+
statusText: 'OK',
|
|
42
|
+
headers: {},
|
|
43
|
+
config,
|
|
44
|
+
data: r.data,
|
|
45
|
+
request: {},
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function restoreAdapter() {
|
|
50
|
+
axios.defaults.adapter = originalAdapter;
|
|
51
|
+
currentHandler = null;
|
|
52
|
+
}
|
|
53
|
+
function setHandler(h) {
|
|
54
|
+
currentHandler = h;
|
|
55
|
+
}
|
|
56
|
+
function buildConfig() {
|
|
57
|
+
return {
|
|
58
|
+
id: 'test-site',
|
|
59
|
+
name: 'Test Site',
|
|
60
|
+
url: 'https://example.com',
|
|
61
|
+
apiKey: 'respira_test_key',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// -----------------------------------------------------------------------------
|
|
65
|
+
// Tests
|
|
66
|
+
// -----------------------------------------------------------------------------
|
|
67
|
+
test('deleteMedia forwards approval_token on the confirm call (927aa7ed regression)', async () => {
|
|
68
|
+
installMockAdapter();
|
|
69
|
+
try {
|
|
70
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
71
|
+
const client = new WordPressClient(buildConfig());
|
|
72
|
+
// First call: no token yet, simulates the initial delete attempt.
|
|
73
|
+
await client.deleteMedia(42);
|
|
74
|
+
assert.equal(callLog.length, 1);
|
|
75
|
+
assert.equal(callLog[0].method, 'delete');
|
|
76
|
+
assert.equal(callLog[0].url, '/media/42');
|
|
77
|
+
assert.ok(!callLog[0].params || callLog[0].params.approval_token === undefined, 'first call must not send an approval_token');
|
|
78
|
+
// Second call: the agent echoes back the exact token it was handed.
|
|
79
|
+
// Pre-fix this token was silently dropped and the WP side looped forever.
|
|
80
|
+
await client.deleteMedia(42, undefined, 'approve-tok-abc123');
|
|
81
|
+
assert.equal(callLog.length, 2);
|
|
82
|
+
assert.equal(callLog[1].method, 'delete');
|
|
83
|
+
assert.equal(callLog[1].url, '/media/42');
|
|
84
|
+
assert.equal(callLog[1].params?.approval_token, 'approve-tok-abc123', 'confirm call must forward the approval_token verbatim');
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
restoreAdapter();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('deleteMedia forwards force alongside approval_token', async () => {
|
|
91
|
+
installMockAdapter();
|
|
92
|
+
try {
|
|
93
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
94
|
+
const client = new WordPressClient(buildConfig());
|
|
95
|
+
await client.deleteMedia(7, true, 'tok-xyz');
|
|
96
|
+
assert.equal(callLog.length, 1);
|
|
97
|
+
assert.equal(callLog[0].params?.force, true);
|
|
98
|
+
assert.equal(callLog[0].params?.approval_token, 'tok-xyz');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
restoreAdapter();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
test('deletePage approval flow is unaffected (no regression)', async () => {
|
|
105
|
+
installMockAdapter();
|
|
106
|
+
try {
|
|
107
|
+
setHandler(() => ({ status: 200, data: { success: true } }));
|
|
108
|
+
const client = new WordPressClient(buildConfig());
|
|
109
|
+
// No force -> no params sent at all (matches pre-existing behavior).
|
|
110
|
+
await client.deletePage(99);
|
|
111
|
+
assert.equal(callLog[0].url, '/pages/99');
|
|
112
|
+
assert.ok(!callLog[0].params || Object.keys(callLog[0].params).length === 0);
|
|
113
|
+
// force + approval_token -> both forwarded, plus confirm_live_edit which
|
|
114
|
+
// deletePage has always sent alongside force.
|
|
115
|
+
await client.deletePage(99, true, 'page-tok-1');
|
|
116
|
+
const call = callLog[1];
|
|
117
|
+
assert.equal(call.url, '/pages/99');
|
|
118
|
+
assert.equal(call.params.force, true);
|
|
119
|
+
assert.equal(call.params.confirm_live_edit, true);
|
|
120
|
+
assert.equal(call.params.approval_token, 'page-tok-1');
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
restoreAdapter();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=delete-media-approval-token.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delete-media-approval-token.test.js","sourceRoot":"","sources":["../../src/__tests__/delete-media-approval-token.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-media-path-detection.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/upload-media-path-detection.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { isLocalFilePath, resolveLocalFilePath } from '../wordpress-client.js';
|
|
8
|
+
// Ticket 5a19d816: on Windows, respira_upload_media(file="C:/Users/.../photo.jpg")
|
|
9
|
+
// fell through the local-file detection (which only matched '/', './', '../',
|
|
10
|
+
// '~') into the base64 branch. Buffer.from(nonBase64, 'base64') doesn't throw,
|
|
11
|
+
// it just silently decodes garbage bytes, so the upload "succeeded" with a
|
|
12
|
+
// corrupt ~80-byte file instead of reading the real one from disk.
|
|
13
|
+
test('isLocalFilePath: Windows drive-letter paths are detected as local files, not base64', () => {
|
|
14
|
+
assert.equal(isLocalFilePath('C:/Users/bob/photo.jpg'), true);
|
|
15
|
+
assert.equal(isLocalFilePath('C:\\Users\\bob\\photo.jpg'), true);
|
|
16
|
+
assert.equal(isLocalFilePath('D:\\Media\\file.png'), true);
|
|
17
|
+
assert.equal(isLocalFilePath('z:/lower/drive/letter.gif'), true);
|
|
18
|
+
});
|
|
19
|
+
test('isLocalFilePath: file:// URIs are detected as local files (Windows and Unix forms)', () => {
|
|
20
|
+
assert.equal(isLocalFilePath('file:///C:/Users/bob/photo.jpg'), true);
|
|
21
|
+
assert.equal(isLocalFilePath('file:///home/bob/photo.jpg'), true);
|
|
22
|
+
});
|
|
23
|
+
test('isLocalFilePath: existing POSIX-style detection is unchanged (no regression)', () => {
|
|
24
|
+
assert.equal(isLocalFilePath('/absolute/unix/path.jpg'), true);
|
|
25
|
+
assert.equal(isLocalFilePath('./relative/path.jpg'), true);
|
|
26
|
+
assert.equal(isLocalFilePath('../parent/path.jpg'), true);
|
|
27
|
+
assert.equal(isLocalFilePath('~/home/path.jpg'), true);
|
|
28
|
+
});
|
|
29
|
+
test('isLocalFilePath: base64 payloads and data:/http(s): URLs are still NOT treated as local files', () => {
|
|
30
|
+
// A plausible base64 blob with no path-like prefix.
|
|
31
|
+
assert.equal(isLocalFilePath('aGVsbG8gd29ybGQgdGhpcyBpcyBhIHRlc3Q='), false);
|
|
32
|
+
assert.equal(isLocalFilePath('data:image/png;base64,iVBORw0KGgo='), false);
|
|
33
|
+
assert.equal(isLocalFilePath('http://example.com/photo.jpg'), false);
|
|
34
|
+
assert.equal(isLocalFilePath('https://example.com/photo.jpg'), false);
|
|
35
|
+
});
|
|
36
|
+
test('resolveLocalFilePath: Windows drive-letter paths pass through unchanged (no cwd corruption)', () => {
|
|
37
|
+
// Regression guard: node:path's resolve() is POSIX on non-Windows hosts and
|
|
38
|
+
// does not recognize "C:\..." as absolute, so running it through resolve()
|
|
39
|
+
// would wrongly prefix the string with cwd. The fix must return the drive
|
|
40
|
+
// path verbatim so fs calls on an actual Windows host see the real path
|
|
41
|
+
// (and, off-Windows, this test suite sees a clean "File not found: C:\..."
|
|
42
|
+
// instead of a mangled "<cwd>/C:\...").
|
|
43
|
+
assert.equal(resolveLocalFilePath('C:\\Users\\bob\\photo.jpg'), 'C:\\Users\\bob\\photo.jpg');
|
|
44
|
+
assert.equal(resolveLocalFilePath('C:/Users/bob/photo.jpg'), 'C:/Users/bob/photo.jpg');
|
|
45
|
+
});
|
|
46
|
+
test('resolveLocalFilePath: file:///C:/... extracts a Windows-style path', () => {
|
|
47
|
+
const resolved = resolveLocalFilePath('file:///C:/Users/bob/photo.jpg');
|
|
48
|
+
assert.equal(resolved, 'C:\\Users\\bob\\photo.jpg');
|
|
49
|
+
});
|
|
50
|
+
test('resolveLocalFilePath: file:///... on Unix round-trips to the real file, byte for byte', () => {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), 'respira-upload-media-test-'));
|
|
52
|
+
const filePath = join(dir, 'photo.jpg');
|
|
53
|
+
writeFileSync(filePath, 'not-actually-a-jpeg-but-that-is-fine');
|
|
54
|
+
try {
|
|
55
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
56
|
+
assert.equal(isLocalFilePath(fileUrl), true);
|
|
57
|
+
const resolved = resolveLocalFilePath(fileUrl);
|
|
58
|
+
assert.equal(resolved, filePath);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('resolveLocalFilePath: existing "~" and relative-path resolution behavior is unchanged', () => {
|
|
65
|
+
const home = process.env.HOME || '';
|
|
66
|
+
assert.equal(resolveLocalFilePath('~/photo.jpg'), `${home}/photo.jpg`);
|
|
67
|
+
// A relative path resolves against cwd, same as plain node:path resolve().
|
|
68
|
+
assert.equal(resolveLocalFilePath('./photo.jpg'), join(process.cwd(), 'photo.jpg'));
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=upload-media-path-detection.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-media-path-detection.test.js","sourceRoot":"","sources":["../../src/__tests__/upload-media-path-detection.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-emitter-privacy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/usage-emitter-privacy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { isDirectTelemetryBearer } from '../usage-emitter.js';
|
|
4
|
+
describe('usage emitter credential privacy', () => {
|
|
5
|
+
it('never treats a local WordPress key as a central-ingest bearer', () => {
|
|
6
|
+
assert.equal(isDirectTelemetryBearer('respira_local-secret'), false);
|
|
7
|
+
assert.equal(isDirectTelemetryBearer('respira_0123456789abcdef'), false);
|
|
8
|
+
});
|
|
9
|
+
it('accepts only explicitly central-scoped bearer families', () => {
|
|
10
|
+
assert.equal(isDirectTelemetryBearer('respira_site_abc'), true);
|
|
11
|
+
assert.equal(isDirectTelemetryBearer('rsp_at_abc'), true);
|
|
12
|
+
assert.equal(isDirectTelemetryBearer('rp_mcp_abc'), true);
|
|
13
|
+
assert.equal(isDirectTelemetryBearer('rp_otel_abc'), false);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=usage-emitter-privacy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-emitter-privacy.test.js","sourceRoot":"","sources":["../../src/__tests__/usage-emitter-privacy.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IAChD,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,sBAAsB,CAAC,EAAE,KAAK,CAAC,CAAC;QACrE,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,0BAA0B,CAAC,EAAE,KAAK,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,kBAAkB,CAAC,EAAE,IAAI,CAAC,CAAC;QAChE,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-retry-safety.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/write-retry-safety.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { ensureWriteIdempotencyKey, isReplaySafeRetry, } from "../wordpress-client.js";
|
|
4
|
+
test("mutations receive one stable idempotency key while reads do not", () => {
|
|
5
|
+
const write = { method: "post", headers: {} };
|
|
6
|
+
ensureWriteIdempotencyKey(write, () => "call_12345678");
|
|
7
|
+
ensureWriteIdempotencyKey(write, () => "should_not_replace");
|
|
8
|
+
assert.equal(write.headers["Idempotency-Key"], "call_12345678");
|
|
9
|
+
const read = { method: "get", headers: {} };
|
|
10
|
+
ensureWriteIdempotencyKey(read, () => "should_not_exist");
|
|
11
|
+
assert.equal(read.headers["Idempotency-Key"], undefined);
|
|
12
|
+
});
|
|
13
|
+
test("reads retry transient responses and connection failures", () => {
|
|
14
|
+
assert.equal(isReplaySafeRetry({ method: "GET", status: 500 }), true);
|
|
15
|
+
assert.equal(isReplaySafeRetry({ method: "GET", connectionCode: "ECONNRESET" }), true);
|
|
16
|
+
assert.equal(isReplaySafeRetry({ method: "GET", status: 404 }), false);
|
|
17
|
+
});
|
|
18
|
+
test("writes retry only explicit replay-safe gateway responses", () => {
|
|
19
|
+
assert.equal(isReplaySafeRetry({
|
|
20
|
+
method: "POST",
|
|
21
|
+
status: 503,
|
|
22
|
+
idempotencySupported: true,
|
|
23
|
+
}), true);
|
|
24
|
+
assert.equal(isReplaySafeRetry({
|
|
25
|
+
method: "POST",
|
|
26
|
+
status: 503,
|
|
27
|
+
idempotencySupported: false,
|
|
28
|
+
}), false);
|
|
29
|
+
assert.equal(isReplaySafeRetry({
|
|
30
|
+
method: "POST",
|
|
31
|
+
status: 500,
|
|
32
|
+
idempotencySupported: true,
|
|
33
|
+
}), false);
|
|
34
|
+
assert.equal(isReplaySafeRetry({ method: "POST", connectionCode: "ECONNRESET" }), false);
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=write-retry-safety.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-retry-safety.test.js","sourceRoot":"","sources":["../../src/__tests__/write-retry-safety.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EACL,yBAAyB,EACzB,iBAAiB,GAClB,MAAM,wBAAwB,CAAC;AAEhC,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,MAAM,KAAK,GAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACnD,yBAAyB,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,eAAe,CAAC,CAAC;IACxD,yBAAyB,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,CAAC;IAC7D,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,eAAe,CAAC,CAAC;IAEhE,MAAM,IAAI,GAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACjD,yBAAyB,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,CAAC;IAC1D,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC,CAAC;AAC3D,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;IACnE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACtE,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,EAClE,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;AACzE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,0DAA0D,EAAE,GAAG,EAAE;IACpE,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC;QAChB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG;QACX,oBAAoB,EAAE,IAAI;KAC3B,CAAC,EACF,IAAI,CACL,CAAC;IACF,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC;QAChB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG;QACX,oBAAoB,EAAE,KAAK;KAC5B,CAAC,EACF,KAAK,CACN,CAAC;IACF,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC;QAChB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG;QACX,oBAAoB,EAAE,IAAI;KAC3B,CAAC,EACF,KAAK,CACN,CAAC;IACF,MAAM,CAAC,KAAK,CACV,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,EACnE,KAAK,CACN,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
package/dist/server.d.ts
CHANGED
|
@@ -203,6 +203,8 @@ export declare class RespiraWordPressServer {
|
|
|
203
203
|
*/
|
|
204
204
|
private normalizeArgsForBackCompat;
|
|
205
205
|
private handleToolCall;
|
|
206
|
+
/** Privacy-safe target HMAC: arguments never leave this process. */
|
|
207
|
+
private telemetryArgsHash;
|
|
206
208
|
/**
|
|
207
209
|
* v6.18.2: enforce a response-size cap on tool results.
|
|
208
210
|
*
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAqBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAsQzE,qBAAa,sBAAsB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,KAAK,CAA2C;IACxD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,YAAY,CAA4B;IAChD,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAS;IACpC,iFAAiF;IACjF,OAAO,CAAC,iBAAiB,CAAK;IAC9B,iFAAiF;IACjF,OAAO,CAAC,mBAAmB,CAAgC;IAE3D,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAsB;IAEhE;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;gBA4Bb,WAAW,EAAE,mBAAmB,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE;IAiVvE,OAAO,CAAC,cAAc;IAItB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,aAAa;IA4BrB,gEAAgE;IAChE,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,UAAU;IA2CxB;;;;;;;;;;;;;;OAcG;YACW,WAAW;IAiJzB;;;;;;;;;OASG;YACW,kBAAkB;IAiHhC;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;YACW,qBAAqB;YAiBrB,kBAAkB;IA6FhC,yFAAyF;IACzF,OAAO,CAAC,gBAAgB;IASxB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAmQP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA+vFtB;;;;;;OAMG;IACH,yEAAyE;IACzE,OAAO,CAAC,mBAAmB,CAAoD;IAC/E,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAU;YAEpC,oBAAoB;YAqDpB,2BAA2B;IAazC;;;;OAIG;YACW,cAAc;IAY5B,OAAO,CAAC,mBAAmB;IA2uC3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IA4G5B,oEAAoE;IACpE,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IAimC9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAwUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
|
package/dist/server.js
CHANGED
|
@@ -11,12 +11,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { dirname, join, resolve } from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
|
+
import { createHmac, randomUUID } from 'node:crypto';
|
|
14
15
|
import { WordPressClient } from './wordpress-client.js';
|
|
15
16
|
import { RespiraVersionChecker } from './version-checker.js';
|
|
16
17
|
import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
|
|
17
18
|
import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
|
|
18
19
|
import { getAcfTools, resolveAcfToolName } from './acf-tools.js';
|
|
19
20
|
import { getUsageEmitter, deriveToolKind } from './usage-emitter.js';
|
|
21
|
+
// Process-local secret keeps target hashes useful for same-session retry
|
|
22
|
+
// detection without making low-entropy WordPress ids reversible centrally.
|
|
23
|
+
const TELEMETRY_HASH_SECRET = randomUUID();
|
|
20
24
|
/**
|
|
21
25
|
* Read the server version from package.json at module load time so the
|
|
22
26
|
* MCP handshake, the `instructions` block, and the version-checker all
|
|
@@ -617,7 +621,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
617
621
|
// gracefully when neither is.
|
|
618
622
|
if (config.apiKey && config.url) {
|
|
619
623
|
try {
|
|
620
|
-
getUsageEmitter().registerSiteToken(config.url, config.apiKey);
|
|
624
|
+
getUsageEmitter().registerSiteToken(config.url, config.apiKey, () => client.getTelemetryToken());
|
|
621
625
|
}
|
|
622
626
|
catch {
|
|
623
627
|
// Best-effort; usage telemetry never blocks server startup.
|
|
@@ -1161,7 +1165,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
1161
1165
|
this.defaultSiteId = s.id;
|
|
1162
1166
|
}
|
|
1163
1167
|
try {
|
|
1164
|
-
getUsageEmitter().registerSiteToken(s.url, s.apiKey);
|
|
1168
|
+
getUsageEmitter().registerSiteToken(s.url, s.apiKey, () => client.getTelemetryToken());
|
|
1165
1169
|
}
|
|
1166
1170
|
catch {
|
|
1167
1171
|
// usage telemetry never blocks
|
|
@@ -5784,13 +5788,16 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5784
5788
|
finally {
|
|
5785
5789
|
if (emitter.isEnabled()) {
|
|
5786
5790
|
let siteUrl = null;
|
|
5791
|
+
let pluginVersion = null;
|
|
5787
5792
|
try {
|
|
5788
5793
|
if (!SITE_AGNOSTIC_TOOLS.has(canonical)) {
|
|
5789
5794
|
const client = this.resolveClient(args);
|
|
5790
5795
|
siteUrl = client.getSiteUrl();
|
|
5796
|
+
pluginVersion = client.pluginVersion;
|
|
5791
5797
|
}
|
|
5792
5798
|
else if (this.currentSite) {
|
|
5793
5799
|
siteUrl = this.currentSite.getSiteUrl();
|
|
5800
|
+
pluginVersion = this.currentSite.pluginVersion;
|
|
5794
5801
|
}
|
|
5795
5802
|
}
|
|
5796
5803
|
catch {
|
|
@@ -5845,6 +5852,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5845
5852
|
durationMs: Date.now() - startedAt,
|
|
5846
5853
|
success: dispatchError === null,
|
|
5847
5854
|
errorCode,
|
|
5855
|
+
targetArgsHash: this.telemetryArgsHash(canonical, args),
|
|
5856
|
+
stageTimings: result?.write_diag?.stage_timings || result?.stage_timings || null,
|
|
5857
|
+
pluginVersion,
|
|
5858
|
+
mcpVersion: MCP_SERVER_VERSION,
|
|
5848
5859
|
});
|
|
5849
5860
|
}
|
|
5850
5861
|
}
|
|
@@ -5857,6 +5868,28 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
5857
5868
|
}
|
|
5858
5869
|
return this.capResponseSize(canonical, result);
|
|
5859
5870
|
}
|
|
5871
|
+
/** Privacy-safe target HMAC: arguments never leave this process. */
|
|
5872
|
+
telemetryArgsHash(toolName, args) {
|
|
5873
|
+
try {
|
|
5874
|
+
const canonicalize = (value) => {
|
|
5875
|
+
if (Array.isArray(value))
|
|
5876
|
+
return value.map(canonicalize);
|
|
5877
|
+
if (value && typeof value === 'object') {
|
|
5878
|
+
return Object.keys(value).sort().reduce((out, key) => {
|
|
5879
|
+
out[key] = canonicalize(value[key]);
|
|
5880
|
+
return out;
|
|
5881
|
+
}, {});
|
|
5882
|
+
}
|
|
5883
|
+
return value;
|
|
5884
|
+
};
|
|
5885
|
+
return createHmac('sha256', TELEMETRY_HASH_SECRET)
|
|
5886
|
+
.update(`${toolName}|${JSON.stringify(canonicalize(args))}`)
|
|
5887
|
+
.digest('hex');
|
|
5888
|
+
}
|
|
5889
|
+
catch {
|
|
5890
|
+
return null;
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5860
5893
|
/**
|
|
5861
5894
|
* v6.18.2: enforce a response-size cap on tool results.
|
|
5862
5895
|
*
|
|
@@ -6368,7 +6401,7 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
|
|
|
6368
6401
|
case 'wordpress_update_media_batch':
|
|
6369
6402
|
return await client.updateMediaBatch(args.items);
|
|
6370
6403
|
case 'wordpress_delete_media':
|
|
6371
|
-
return await client.deleteMedia(args.id);
|
|
6404
|
+
return await client.deleteMedia(args.id, args.force, args.approval_token);
|
|
6372
6405
|
// Menu Management
|
|
6373
6406
|
case 'wordpress_list_menus':
|
|
6374
6407
|
return await client.listMenus();
|