@respira/wordpress-mcp-server 6.19.7 → 6.19.9

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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tests for buildDefaultSiteRoutingNotice — the multi-site safety notice that
3
+ * surfaces on the response envelope when a WRITE tool runs against the default
4
+ * site because the caller omitted site_id on an account with more than one
5
+ * site connected.
6
+ *
7
+ * Background: Claude Desktop frequently omits site_id and doesn't reliably call
8
+ * respira_switch_site, so an edit meant for site B silently lands on the
9
+ * default (first) site. C.J. 2026-05-29 reported exactly this ("the token for
10
+ * the new site is resolving to the first one"). The notice makes the
11
+ * wrong-site write visible the moment it happens.
12
+ *
13
+ * The method is private; the test reaches it through `as any` since `private`
14
+ * is a compile-time-only constraint and these run against compiled JS.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=default-site-routing-notice.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-site-routing-notice.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/default-site-routing-notice.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tests for buildDefaultSiteRoutingNotice — the multi-site safety notice that
3
+ * surfaces on the response envelope when a WRITE tool runs against the default
4
+ * site because the caller omitted site_id on an account with more than one
5
+ * site connected.
6
+ *
7
+ * Background: Claude Desktop frequently omits site_id and doesn't reliably call
8
+ * respira_switch_site, so an edit meant for site B silently lands on the
9
+ * default (first) site. C.J. 2026-05-29 reported exactly this ("the token for
10
+ * the new site is resolving to the first one"). The notice makes the
11
+ * wrong-site write visible the moment it happens.
12
+ *
13
+ * The method is private; the test reaches it through `as any` since `private`
14
+ * is a compile-time-only constraint and these run against compiled JS.
15
+ */
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import { RespiraWordPressServer } from '../server.js';
19
+ const SITE_A = {
20
+ id: 'thegoldenrhodes-com',
21
+ name: 'The Golden Rhodes',
22
+ url: 'https://thegoldenrhodes.com',
23
+ apiKey: 'respira_token_a',
24
+ default: true,
25
+ };
26
+ const SITE_B = {
27
+ id: 'shivamandir-org',
28
+ name: 'Shivamandir',
29
+ url: 'https://shivamandir.org',
30
+ apiKey: 'respira_token_b',
31
+ };
32
+ function notice(server, name, args) {
33
+ return server.buildDefaultSiteRoutingNotice(name, args);
34
+ }
35
+ test('multi-site + write tool + no site_id → notice naming the default site', () => {
36
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
37
+ const result = notice(server, 'respira_update_element', { element_id: 'abc', content: 'hi' });
38
+ assert.ok(result, 'expected a routing notice');
39
+ assert.match(result, /The Golden Rhodes/);
40
+ assert.match(result, /thegoldenrhodes\.com/);
41
+ assert.match(result, /default site/);
42
+ // It should point the caller at both escape hatches.
43
+ assert.match(result, /site_id/);
44
+ assert.match(result, /respira_switch_site/);
45
+ });
46
+ test('explicit site_id suppresses the notice (the call is already targeted)', () => {
47
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
48
+ const result = notice(server, 'respira_update_element', { site_id: 'shivamandir-org', element_id: 'abc' });
49
+ assert.equal(result, null);
50
+ });
51
+ test('read tools never get a notice, even on a multi-site account', () => {
52
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
53
+ assert.equal(notice(server, 'respira_list_pages', {}), null);
54
+ assert.equal(notice(server, 'respira_read_page', { page_id: 5 }), null);
55
+ assert.equal(notice(server, 'respira_get_site_context', {}), null);
56
+ });
57
+ test('single-site accounts never get a notice (no wrong-site risk)', () => {
58
+ const server = new RespiraWordPressServer([SITE_A]);
59
+ assert.equal(notice(server, 'respira_update_element', { element_id: 'abc' }), null);
60
+ });
61
+ test('write tool with no args at all on multi-site still warns', () => {
62
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
63
+ const result = notice(server, 'respira_delete_page', undefined);
64
+ assert.ok(result, 'expected a routing notice when args is undefined');
65
+ assert.match(result, /The Golden Rhodes/);
66
+ });
67
+ test('the deprecated wordpress_* alias is classified the same as respira_*', () => {
68
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
69
+ const result = notice(server, 'wordpress_update_page', { page_id: 1 });
70
+ assert.ok(result, 'wordpress_* write alias should also warn');
71
+ });
72
+ // ── RESPIRA_REQUIRE_SITE_ID hard guard (v6.19.8) ──────────────────────────
73
+ // enforceSiteScopeForWrites throws when the flag is on, the account is
74
+ // multi-site, the tool is a write, and no site_id was passed. It's the
75
+ // prevention counterpart to the visibility notice above.
76
+ function enforce(server, name, args) {
77
+ server.enforceSiteScopeForWrites(name, args);
78
+ }
79
+ function withFlag(value, fn) {
80
+ const prev = process.env.RESPIRA_REQUIRE_SITE_ID;
81
+ if (value === undefined)
82
+ delete process.env.RESPIRA_REQUIRE_SITE_ID;
83
+ else
84
+ process.env.RESPIRA_REQUIRE_SITE_ID = value;
85
+ try {
86
+ fn();
87
+ }
88
+ finally {
89
+ if (prev === undefined)
90
+ delete process.env.RESPIRA_REQUIRE_SITE_ID;
91
+ else
92
+ process.env.RESPIRA_REQUIRE_SITE_ID = prev;
93
+ }
94
+ }
95
+ test('guard off by default: multi-site write without site_id does not throw', () => {
96
+ withFlag(undefined, () => {
97
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
98
+ assert.doesNotThrow(() => enforce(server, 'respira_inject_builder_content', { page_id: 9, builder: 'elementor', content: {} }));
99
+ });
100
+ });
101
+ test('guard on: multi-site write without site_id throws respira_site_id_required', () => {
102
+ withFlag('1', () => {
103
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
104
+ let thrown = null;
105
+ try {
106
+ enforce(server, 'respira_inject_builder_content', { page_id: 9, builder: 'elementor', content: {} });
107
+ }
108
+ catch (e) {
109
+ thrown = e;
110
+ }
111
+ assert.ok(thrown, 'expected the guard to throw');
112
+ assert.equal(thrown.name, 'respira_site_id_required');
113
+ assert.match(thrown.message, /thegoldenrhodes-com/);
114
+ assert.match(thrown.message, /shivamandir-org/);
115
+ });
116
+ });
117
+ test('guard on: explicit site_id is allowed through', () => {
118
+ withFlag('1', () => {
119
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
120
+ assert.doesNotThrow(() => enforce(server, 'respira_inject_builder_content', { site_id: 'shivamandir-org', page_id: 9, builder: 'elementor', content: {} }));
121
+ });
122
+ });
123
+ test('guard on: read tools are never blocked', () => {
124
+ withFlag('true', () => {
125
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
126
+ assert.doesNotThrow(() => enforce(server, 'respira_read_page', { page_id: 5 }));
127
+ assert.doesNotThrow(() => enforce(server, 'respira_list_pages', {}));
128
+ });
129
+ });
130
+ test('guard on: single-site account is never blocked', () => {
131
+ withFlag('1', () => {
132
+ const server = new RespiraWordPressServer([SITE_A]);
133
+ assert.doesNotThrow(() => enforce(server, 'respira_inject_builder_content', { page_id: 9, builder: 'elementor', content: {} }));
134
+ });
135
+ });
136
+ test('guard on: site-agnostic tools (switch_site) are never blocked', () => {
137
+ withFlag('1', () => {
138
+ const server = new RespiraWordPressServer([SITE_A, SITE_B]);
139
+ assert.doesNotThrow(() => enforce(server, 'respira_switch_site', { site_id: 'shivamandir-org' }));
140
+ });
141
+ });
142
+ //# sourceMappingURL=default-site-routing-notice.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-site-routing-notice.test.js","sourceRoot":"","sources":["../../src/__tests__/default-site-routing-notice.test.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -89,6 +89,35 @@ export declare class RespiraWordPressServer {
89
89
  * a structured error so the AI can explain the failure plainly.
90
90
  */
91
91
  private redeemInstallToken;
92
+ /**
93
+ * When a WRITE tool runs against the default site because the caller
94
+ * omitted site_id and the account has more than one site connected,
95
+ * surface a one-line routing notice on the response. This is the
96
+ * wrong-site window: Claude Desktop frequently omits site_id and doesn't
97
+ * reliably call respira_switch_site, so an edit meant for site B silently
98
+ * lands on the default (first) site. Naming the acted-on site in the
99
+ * envelope makes that visible the moment the write happens, instead of
100
+ * after the user notices the wrong page changed. C.J. 2026-05-29 reported
101
+ * exactly this ("the token for the new site is resolving to the first one").
102
+ *
103
+ * Read tools and explicit-site_id calls get no notice — only the silent
104
+ * default-routing case on a multi-site account is worth flagging.
105
+ */
106
+ private buildDefaultSiteRoutingNotice;
107
+ /**
108
+ * Opt-in strict scoping for write tools. When RESPIRA_REQUIRE_SITE_ID is
109
+ * truthy and the account has more than one site connected, a WRITE tool
110
+ * called without an explicit site_id throws instead of silently using the
111
+ * default site. Read tools, single-site accounts, site-agnostic tools, and
112
+ * calls that already carry site_id are never blocked.
113
+ *
114
+ * The thrown error carries name `respira_site_id_required` so the
115
+ * admin/mcp-quality dashboard groups it distinctly, and the message lists
116
+ * the connected sites so the agent can immediately retry with the right id.
117
+ *
118
+ * @since 6.19.8
119
+ */
120
+ private enforceSiteScopeForWrites;
92
121
  private withSiteContext;
93
122
  private setupHandlers;
94
123
  private attachUpdateNotice;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAiLzE,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;IAEpC,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;IAmUvE,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;IAyIzB;;;;;;;;;OASG;YACW,kBAAkB;IA6FhC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YAuOP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA28EtB;;;;;;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;IAwT3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IAqG5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA+wB9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,OAAO,KAAK,EAAE,mBAAmB,EAAe,MAAM,kBAAkB,CAAC;AAiLzE,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;IAEpC,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;IAmUvE,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;IAyIzB;;;;;;;;;OASG;YACW,kBAAkB;IA6FhC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,6BAA6B;IA0BrC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,yBAAyB;IA+BjC,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,aAAa;YA2OP,kBAAkB;YA6BlB,yBAAyB;IASvC;;;OAGG;IACH,OAAO,CAAC,oBAAoB;YAyBd,QAAQ;IA28EtB;;;;;;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;IAwT3B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAe3C;IAEF;;;;OAIG;IACH,OAAO,CAAC,0BAA0B;YAmCpB,cAAc;IAqG5B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,eAAe;YAuCT,gBAAgB;IA2xB9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoUzB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IA6UxB,GAAG;CAyCV"}
package/dist/server.js CHANGED
@@ -16,7 +16,7 @@ import { RespiraVersionChecker } from './version-checker.js';
16
16
  import { getBricksTools, dispatchBricksTool } from './bricks-tools.js';
17
17
  import { getElementorTools, dispatchElementorTool } from './elementor-tools.js';
18
18
  import { getAcfTools, ACF_TOOL_NAMES } from './acf-tools.js';
19
- import { getUsageEmitter } from './usage-emitter.js';
19
+ import { getUsageEmitter, deriveToolKind } from './usage-emitter.js';
20
20
  /**
21
21
  * Read the server version from package.json at module load time so the
22
22
  * MCP handshake, the `instructions` block, and the version-checker all
@@ -928,6 +928,83 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
928
928
  message: `Connected ${sites.length} site${sites.length === 1 ? '' : 's'} via the Respira Cowork token. The config was written to ~/.respira/config.json. Restart this Cowork chat (or open a new one) so the MCP server picks up the new sites.`,
929
929
  };
930
930
  }
931
+ /**
932
+ * When a WRITE tool runs against the default site because the caller
933
+ * omitted site_id and the account has more than one site connected,
934
+ * surface a one-line routing notice on the response. This is the
935
+ * wrong-site window: Claude Desktop frequently omits site_id and doesn't
936
+ * reliably call respira_switch_site, so an edit meant for site B silently
937
+ * lands on the default (first) site. Naming the acted-on site in the
938
+ * envelope makes that visible the moment the write happens, instead of
939
+ * after the user notices the wrong page changed. C.J. 2026-05-29 reported
940
+ * exactly this ("the token for the new site is resolving to the first one").
941
+ *
942
+ * Read tools and explicit-site_id calls get no notice — only the silent
943
+ * default-routing case on a multi-site account is worth flagging.
944
+ */
945
+ buildDefaultSiteRoutingNotice(name, args) {
946
+ if (this.sites.size <= 1 || !this.currentSite) {
947
+ return null;
948
+ }
949
+ const hasExplicitSiteId = args && typeof args === 'object' && typeof args.site_id === 'string' && args.site_id.length > 0;
950
+ if (hasExplicitSiteId) {
951
+ return null;
952
+ }
953
+ if (deriveToolKind(this.normalizeToolName(name).canonical) !== 'write') {
954
+ return null;
955
+ }
956
+ const site = this.currentSite;
957
+ let host = site.getSiteId();
958
+ try {
959
+ host = new URL(site.getSiteUrl()).host;
960
+ }
961
+ catch {
962
+ // fall back to the site id
963
+ }
964
+ return (`Wrote to ${site.getSiteName()} (${host}) — your default site, because no site_id was given. ` +
965
+ `${this.sites.size} sites are connected. To target a different site, pass site_id on the tool call ` +
966
+ `or call respira_switch_site first.`);
967
+ }
968
+ /**
969
+ * Opt-in strict scoping for write tools. When RESPIRA_REQUIRE_SITE_ID is
970
+ * truthy and the account has more than one site connected, a WRITE tool
971
+ * called without an explicit site_id throws instead of silently using the
972
+ * default site. Read tools, single-site accounts, site-agnostic tools, and
973
+ * calls that already carry site_id are never blocked.
974
+ *
975
+ * The thrown error carries name `respira_site_id_required` so the
976
+ * admin/mcp-quality dashboard groups it distinctly, and the message lists
977
+ * the connected sites so the agent can immediately retry with the right id.
978
+ *
979
+ * @since 6.19.8
980
+ */
981
+ enforceSiteScopeForWrites(name, args) {
982
+ const flag = (process.env.RESPIRA_REQUIRE_SITE_ID || '').trim().toLowerCase();
983
+ const enabled = flag === '1' || flag === 'true' || flag === 'yes' || flag === 'on';
984
+ if (!enabled) {
985
+ return;
986
+ }
987
+ if (this.sites.size <= 1) {
988
+ return;
989
+ }
990
+ const canonical = this.normalizeToolName(name).canonical;
991
+ if (SITE_AGNOSTIC_TOOLS.has(canonical)) {
992
+ return;
993
+ }
994
+ if (deriveToolKind(canonical) !== 'write') {
995
+ return;
996
+ }
997
+ const hasExplicitSiteId = args && typeof args === 'object' && typeof args.site_id === 'string' && args.site_id.length > 0;
998
+ if (hasExplicitSiteId) {
999
+ return;
1000
+ }
1001
+ const available = Array.from(this.sites.keys()).join(', ') || '(none configured)';
1002
+ const err = new Error(`RESPIRA_REQUIRE_SITE_ID is enabled and this is a multi-site configuration, so write tools must name their target. ` +
1003
+ `Pass site_id on this ${canonical} call. Available site_id values: ${available}. ` +
1004
+ `(This guard prevents an omitted site_id from silently writing to the default site.)`);
1005
+ err.name = 'respira_site_id_required';
1006
+ throw err;
1007
+ }
931
1008
  withSiteContext(result, args) {
932
1009
  const site = this.getActiveSiteSummary(args);
933
1010
  if (!site) {
@@ -1018,6 +1095,10 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
1018
1095
  };
1019
1096
  }
1020
1097
  const resultWithSite = this.withSiteContext(result, args);
1098
+ const routingNotice = this.buildDefaultSiteRoutingNotice(name, args);
1099
+ if (routingNotice && resultWithSite && typeof resultWithSite === 'object' && !Array.isArray(resultWithSite)) {
1100
+ resultWithSite.routing_notice = routingNotice;
1101
+ }
1021
1102
  const resultWithNotice = await this.attachUpdateNotice(resultWithSite);
1022
1103
  const resultWithVersionWarning = this.attachVersionWarning(resultWithNotice);
1023
1104
  return {
@@ -4318,6 +4399,17 @@ Allowlist: css, scss, less, json. PHP / JS theme writes are intentionally out of
4318
4399
  return envelope;
4319
4400
  }
4320
4401
  async dispatchToolCall(name, args) {
4402
+ // v6.19.8: opt-in strict site scoping. When RESPIRA_REQUIRE_SITE_ID is set
4403
+ // and the account has >1 site connected, a WRITE tool called WITHOUT an
4404
+ // explicit site_id is refused instead of silently routing to the default
4405
+ // (first) site. This is the prevention counterpart to the
4406
+ // buildDefaultSiteRoutingNotice visibility notice: a notice tells you the
4407
+ // write hit the wrong site AFTER it landed; this stops it landing at all.
4408
+ // T.S. (studioscaler) needs hard per-session isolation in a multi-tenant
4409
+ // Cowork setup where switch_site mutates global state across sessions.
4410
+ // Default off, so single-site and switch_site-based multi-site users are
4411
+ // unaffected.
4412
+ this.enforceSiteScopeForWrites(name, args);
4321
4413
  // v6.12.0: per-call site resolution. Agnostic tools work without a
4322
4414
  // resolved client; everything else requires either args.site_id or a
4323
4415
  // global currentSite. resolveClient throws cleanly if neither is set.