@wickedevolutions/abilities-mcp 1.3.1 → 1.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +88 -17
  3. package/abilities-mcp.js +191 -114
  4. package/lib/auth/bridge-identity-provider.js +34 -0
  5. package/lib/auth/browser-launcher.js +67 -0
  6. package/lib/auth/config-migration.js +322 -0
  7. package/lib/auth/dcr-client.js +123 -0
  8. package/lib/auth/discovery-client.js +273 -0
  9. package/lib/auth/errors.js +114 -0
  10. package/lib/auth/events.js +55 -0
  11. package/lib/auth/fresh-each-time-identity.js +101 -0
  12. package/lib/auth/http-json.js +151 -0
  13. package/lib/auth/index.js +88 -0
  14. package/lib/auth/keychain-secret-store.js +265 -0
  15. package/lib/auth/loopback-server.js +249 -0
  16. package/lib/auth/memory-secret-store.js +0 -0
  17. package/lib/auth/oauth-client.js +357 -0
  18. package/lib/auth/pkce.js +93 -0
  19. package/lib/auth/schema-v2.js +110 -0
  20. package/lib/auth/secret-store.js +78 -0
  21. package/lib/auth/token-manager.js +378 -0
  22. package/lib/cli/commands/add-site.js +226 -0
  23. package/lib/cli/commands/force-downgrade.js +93 -0
  24. package/lib/cli/commands/list-sites.js +93 -0
  25. package/lib/cli/commands/reauth.js +108 -0
  26. package/lib/cli/commands/revoke.js +127 -0
  27. package/lib/cli/commands/self-check.js +158 -0
  28. package/lib/cli/commands/test.js +174 -0
  29. package/lib/cli/commands/upgrade-auth.js +259 -0
  30. package/lib/cli/config-store.js +328 -0
  31. package/lib/cli/context.js +102 -0
  32. package/lib/cli/errors.js +227 -0
  33. package/lib/cli/index.js +173 -0
  34. package/lib/cli/output.js +175 -0
  35. package/lib/cli/parse-args.js +80 -0
  36. package/lib/config-source-line.js +85 -0
  37. package/lib/config.js +282 -22
  38. package/lib/connection-pool.js +214 -11
  39. package/lib/router.js +29 -11
  40. package/lib/transports/oauth-http-transport.js +601 -0
  41. package/package.json +8 -2
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+
5
+ /**
6
+ * Format the operator-visible startup diagnostic line that names which config
7
+ * source `loadConfig` resolved to and what's in it.
8
+ *
9
+ * Output goes to stderr where Claude Desktop's MCP log captures it
10
+ * (visible in `mcp-server-WordPress (Abilities MCP).log` on macOS), so the
11
+ * operator can tell at a glance:
12
+ * - Whether the .mcpb extension is in env-var single-site mode or has
13
+ * handed off to a home-dir wp-sites.json.
14
+ * - How many sites are configured and what auth method each uses.
15
+ * - Which file path or env var is the source of truth right now.
16
+ *
17
+ * Discriminants set in `lib/config.js`:
18
+ * - 'explicit-config' — args.config / --config=<path>
19
+ * - 'script-adjacent' — wp-sites.json next to abilities-mcp.js
20
+ * - 'home-dir' — ~/.abilities-mcp/wp-sites.json
21
+ * - 'env-var' — ABILITIES_MCP_URL injected by Claude Desktop user_config
22
+ * - 'legacy-cli' — --host / --path (mcp-ssh-bridge backward compat)
23
+ *
24
+ * The line never includes secrets — only site IDs, auth methods, hostnames,
25
+ * tildified file paths, and counts.
26
+ *
27
+ * Copyright (C) 2026 Influencentricity | Wicked Evolutions
28
+ * @license GPL-2.0-or-later
29
+ */
30
+
31
+ /**
32
+ * Replace a leading $HOME prefix with `~/` so logs don't leak the operator's
33
+ * full username path. No-op for paths outside $HOME.
34
+ */
35
+ function tildify(p) {
36
+ if (!p) return p;
37
+ const home = os.homedir();
38
+ if (home && (p === home || p.startsWith(home + '/'))) {
39
+ return '~' + p.slice(home.length);
40
+ }
41
+ return p;
42
+ }
43
+
44
+ /**
45
+ * Per-site short auth label: prefer auth.method (v2 schema), fall back to
46
+ * transport (v1 schema), 'unknown' if neither is set.
47
+ */
48
+ function siteAuthLabel(site) {
49
+ if (site && site.auth && site.auth.method) return site.auth.method;
50
+ if (site && site.transport) return site.transport;
51
+ return 'unknown';
52
+ }
53
+
54
+ /**
55
+ * Build the Config-source line.
56
+ *
57
+ * @param {object} config Output of `loadConfig` — must carry `_configSource`
58
+ * and `_configSourceLabel`.
59
+ * @returns {string} One-line operator diagnostic, no trailing newline.
60
+ */
61
+ function formatConfigSourceLine(config) {
62
+ const source = config && config._configSource;
63
+ const rawLabel = (config && config._configSourceLabel) || '';
64
+
65
+ if (source === 'env-var') {
66
+ const site = config.sites && config.sites[config.defaultSite];
67
+ const username = (site && site.http && site.http.username) || '?';
68
+ return `Config source: ABILITIES_MCP_URL env var (single-site basic auth: ${rawLabel} as ${username})`;
69
+ }
70
+
71
+ if (source === 'legacy-cli') {
72
+ return `Config source: --host/--path legacy CLI (single-site SSH: ${rawLabel})`;
73
+ }
74
+
75
+ // File-based: explicit-config / script-adjacent / home-dir
76
+ const label = tildify(rawLabel);
77
+ const siteEntries = Object.entries(config.sites || {}).map(
78
+ ([id, site]) => `${id} ${siteAuthLabel(site)}`
79
+ );
80
+ const sitesHeader = siteEntries.length === 1 ? '1 site' : `${siteEntries.length} sites`;
81
+ const sourcePrefix = source ? `[${source}] ` : '';
82
+ return `Config source: ${sourcePrefix}${label} (${sitesHeader}: ${siteEntries.join(', ')})`;
83
+ }
84
+
85
+ module.exports = { formatConfigSourceLine, tildify, siteAuthLabel };
package/lib/config.js CHANGED
@@ -1,40 +1,114 @@
1
1
  'use strict';
2
2
 
3
- const { execSync } = require('child_process');
3
+ const { exec } = require('child_process');
4
+ const { promisify } = require('util');
4
5
  const fs = require('fs');
6
+ const fsp = require('fs').promises;
5
7
  const path = require('path');
6
8
  const os = require('os');
7
9
 
10
+ const execAsync = promisify(exec);
11
+
8
12
  /**
9
- * Load configuration from wp-sites.json or build from CLI args.
13
+ * Load configuration from wp-sites.json, environment variables, or CLI args.
10
14
  *
11
- * Search order for wp-sites.json:
15
+ * Search order:
12
16
  * 1. --config=<path> explicit path
13
17
  * 2. Same directory as abilities-mcp.js
14
18
  * 3. ~/.abilities-mcp/wp-sites.json
19
+ * 4. ABILITIES_MCP_URL/USERNAME/PASSWORD env vars (single-site, .mcpb path)
20
+ * 5. --host/--path CLI args (legacy SSH single-site)
15
21
  *
16
- * If no config file and --host/--path provided, builds a single-site legacy config.
22
+ * Per Issue #5 / Phase E.2 (Stretch to Stable sprint): the startup chain
23
+ * — `resolveConfigFilePath`, `loadConfig`, `loadConfigFile`,
24
+ * `validateSiteConfig`, `resolvePassword` — is async so file reads and
25
+ * `passwordCommand` shell-outs do not block the event loop during boot.
17
26
  *
18
27
  * Copyright (C) 2026 Influencentricity | Wicked Evolutions
19
28
  * @license GPL-2.0-or-later
20
29
  */
21
- function loadConfig(args) {
30
+
31
+ /**
32
+ * Async non-throwing existence check. Returns true iff the path resolves
33
+ * via fs.access(); any error (ENOENT, EACCES, …) yields false. Used by the
34
+ * search-order helpers which only care whether a candidate file is readable.
35
+ */
36
+ async function _exists(p) {
37
+ try {
38
+ await fsp.access(p, fs.constants.F_OK);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Resolve the on-disk wp-sites.json path that `loadConfig` would consume,
47
+ * without reading or validating it. Returns `null` when no file path applies
48
+ * (env-var single-site, legacy --host/--path, or no config at all).
49
+ *
50
+ * Used by the v1→v2 migration shim in `abilities-mcp.js` so the migration can
51
+ * run before `loadConfig` parses + validates a stale v1 file.
52
+ *
53
+ * Mirrors steps 1-3 of `loadConfig`'s search order. Steps 4-5 (env / legacy
54
+ * CLI) intentionally return `null` — there is no on-disk file to migrate.
55
+ *
56
+ * @param {object} args
57
+ * @returns {Promise<string|null>} Absolute path of the resolved wp-sites.json, or null.
58
+ */
59
+ async function resolveConfigFilePath(args) {
60
+ if (args && args.config) {
61
+ return path.resolve(args.config);
62
+ }
63
+ const scriptDir = path.resolve(__dirname, '..');
64
+ const scriptConfig = path.join(scriptDir, 'wp-sites.json');
65
+ if (await _exists(scriptConfig)) {
66
+ return scriptConfig;
67
+ }
68
+ const homeConfig = path.join(os.homedir(), '.abilities-mcp', 'wp-sites.json');
69
+ if (await _exists(homeConfig)) {
70
+ return homeConfig;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ async function loadConfig(args) {
22
76
  // Explicit config path
23
77
  if (args.config) {
24
- return loadConfigFile(args.config);
78
+ return loadConfigFile(args.config, 'explicit-config');
25
79
  }
26
80
 
27
81
  // Check alongside script (lib/ → package root)
28
82
  const scriptDir = path.resolve(__dirname, '..');
29
83
  const scriptConfig = path.join(scriptDir, 'wp-sites.json');
30
- if (fs.existsSync(scriptConfig)) {
31
- return loadConfigFile(scriptConfig);
84
+ if (await _exists(scriptConfig)) {
85
+ return loadConfigFile(scriptConfig, 'script-adjacent');
32
86
  }
33
87
 
34
88
  // Check home directory
35
89
  const homeConfig = path.join(os.homedir(), '.abilities-mcp', 'wp-sites.json');
36
- if (fs.existsSync(homeConfig)) {
37
- return loadConfigFile(homeConfig);
90
+ if (await _exists(homeConfig)) {
91
+ return loadConfigFile(homeConfig, 'home-dir');
92
+ }
93
+
94
+ // Env-var single-site config — covers the .mcpb install path and any
95
+ // env-var-based MCP client configuration (claude mcp add, Docker, etc.)
96
+ //
97
+ // First-launch seed (#34): if the .mcpb just installed and no home-dir
98
+ // wp-sites.json exists yet, seed one from the env vars so subsequent CLI
99
+ // commands (`list-sites`, `upgrade-auth`, `add-site`) operate on a single
100
+ // source of truth that already includes the site Claude Desktop is
101
+ // connected to. On success the bridge loads the freshly seeded file —
102
+ // same shape it would read on next restart — so the runtime path stays
103
+ // identical regardless of whether seeding just happened. On failure
104
+ // (keytar unavailable, file-already-exists, write error) the seed is a
105
+ // graceful no-op and we fall back to env-var-only mode below.
106
+ if (process.env.ABILITIES_MCP_URL) {
107
+ const seedResult = await _seedFromEnvIfMissing(homeConfig, process.env);
108
+ if (seedResult && seedResult.seeded) {
109
+ return loadConfigFile(homeConfig, 'home-dir');
110
+ }
111
+ return buildEnvConfig(process.env);
38
112
  }
39
113
 
40
114
  // Legacy CLI mode — single site from --host/--path
@@ -43,17 +117,90 @@ function loadConfig(args) {
43
117
  }
44
118
 
45
119
  throw new Error(
46
- 'No wp-sites.json found and no --host/--path provided.\n' +
47
- 'Create wp-sites.json or use: --host=<ssh-host> --path=<wp-path>'
120
+ 'No configuration found.\n' +
121
+ 'Provide one of: wp-sites.json, ABILITIES_MCP_URL+USERNAME+PASSWORD env vars, or --host/--path.'
48
122
  );
49
123
  }
50
124
 
51
- function loadConfigFile(filePath) {
52
- const raw = fs.readFileSync(filePath, 'utf8');
125
+ /**
126
+ * Build single-site config from env vars (ABILITIES_MCP_URL/USERNAME/PASSWORD).
127
+ *
128
+ * Auto-derives the MCP adapter endpoint from the site URL:
129
+ * https://example.com → https://example.com/wp-json/mcp/mcp-adapter-default-server
130
+ *
131
+ * This is the path used by .mcpb bundles installed in Claude Desktop and any
132
+ * other env-var-based MCP client configuration.
133
+ */
134
+ function buildEnvConfig(env) {
135
+ const rawUrl = env.ABILITIES_MCP_URL;
136
+ const username = env.ABILITIES_MCP_USERNAME;
137
+ const password = env.ABILITIES_MCP_PASSWORD;
138
+
139
+ if (!username) {
140
+ throw new Error('ABILITIES_MCP_URL is set but ABILITIES_MCP_USERNAME is missing');
141
+ }
142
+ if (!password) {
143
+ throw new Error('ABILITIES_MCP_URL is set but ABILITIES_MCP_PASSWORD is missing');
144
+ }
145
+
146
+ let parsedUrl;
147
+ try {
148
+ parsedUrl = new URL(rawUrl);
149
+ } catch (e) {
150
+ throw new Error(`ABILITIES_MCP_URL is not a valid URL: ${rawUrl}`);
151
+ }
152
+
153
+ const isHttps = parsedUrl.protocol === 'https:';
154
+ const isHttp = parsedUrl.protocol === 'http:';
155
+ if (!isHttps && !isHttp) {
156
+ throw new Error(`ABILITIES_MCP_URL must be http or https: ${rawUrl}`);
157
+ }
158
+
159
+ // Strip trailing slash from origin+path, then append the adapter route.
160
+ const base = (parsedUrl.origin + parsedUrl.pathname).replace(/\/+$/, '');
161
+ const endpoint = `${base}/wp-json/mcp/mcp-adapter-default-server`;
162
+
163
+ const siteConfig = {
164
+ label: parsedUrl.hostname,
165
+ url: parsedUrl.origin,
166
+ transport: 'http',
167
+ http: {
168
+ endpoint,
169
+ username,
170
+ password,
171
+ },
172
+ };
173
+
174
+ // Allow plain HTTP only when the operator explicitly opts in. The .mcpb path
175
+ // expects HTTPS by default; localhost dev gets a narrow exception.
176
+ if (isHttp) {
177
+ const isLocal = parsedUrl.hostname === 'localhost' || parsedUrl.hostname === '127.0.0.1';
178
+ if (!isLocal && env.ABILITIES_MCP_ALLOW_INSECURE !== 'true') {
179
+ throw new Error(
180
+ `ABILITIES_MCP_URL is HTTP (not HTTPS): ${rawUrl}\n` +
181
+ `Set ABILITIES_MCP_ALLOW_INSECURE=true to allow plain HTTP.`
182
+ );
183
+ }
184
+ siteConfig.allowInsecure = true;
185
+ }
186
+
187
+ return {
188
+ defaultSite: 'default',
189
+ _isMultiSite: false,
190
+ _configSource: 'env-var',
191
+ _configSourceLabel: parsedUrl.hostname,
192
+ sites: {
193
+ default: siteConfig,
194
+ },
195
+ };
196
+ }
197
+
198
+ async function loadConfigFile(filePath, source = 'explicit-config') {
199
+ const raw = await fsp.readFile(filePath, 'utf8');
53
200
 
54
201
  // Warn if config file is readable by group or world
55
202
  try {
56
- const stat = fs.statSync(filePath);
203
+ const stat = await fsp.stat(filePath);
57
204
  if (stat.mode & 0o077) {
58
205
  process.stderr.write(
59
206
  `WARNING: ${filePath} is readable by group/world (mode ${(stat.mode & 0o777).toString(8)}). ` +
@@ -78,17 +225,69 @@ function loadConfigFile(filePath) {
78
225
 
79
226
  // Validate each site
80
227
  for (const [key, site] of Object.entries(config.sites)) {
81
- validateSiteConfig(key, site);
228
+ await validateSiteConfig(key, site);
82
229
  }
83
230
 
84
231
  config._isMultiSite = Object.keys(config.sites).length > 1 ||
85
232
  Object.values(config.sites).some(s => s.multisite);
86
233
  config._configPath = filePath;
234
+ config._configSource = source;
235
+ config._configSourceLabel = filePath;
87
236
 
88
237
  return config;
89
238
  }
90
239
 
91
- function validateSiteConfig(key, site) {
240
+ async function validateSiteConfig(key, site) {
241
+ // v2 OAuth sites carry no transport block (Appendix F.5 + add-site flow).
242
+ // The runtime treats them as HTTP — endpoint comes from auth.mcp_resource
243
+ // or site.mcp_resource (resolved by the OAuth-aware transport).
244
+ if (site.auth && site.auth.method === 'oauth') {
245
+ if (!site.url) {
246
+ throw new Error(`Site "${key}" (oauth): requires "url"`);
247
+ }
248
+ if (!site.auth.access_token_ref || !site.auth.refresh_token_ref) {
249
+ throw new Error(`Site "${key}" (oauth): requires auth.access_token_ref and auth.refresh_token_ref`);
250
+ }
251
+ if (!site.mcp_resource) {
252
+ throw new Error(`Site "${key}" (oauth): requires mcp_resource (set during add-site / reauth)`);
253
+ }
254
+ if (!site.mcp_resource.startsWith('https://') && !site.allowInsecure) {
255
+ throw new Error(`Site "${key}" (oauth): mcp_resource is not HTTPS. Set "allowInsecure": true on the site to allow HTTP`);
256
+ }
257
+ return;
258
+ }
259
+
260
+ // v2 App-Password sites — produced by config-migration from v1 transport-style
261
+ // configs. Secret lives in keychain (auth.password_ref); the legacy http.password*
262
+ // fields are stripped during migration so the keychain is the sole source of
263
+ // truth. Carrier transport (http or ssh) is preserved for the runtime.
264
+ if (site.auth && site.auth.method === 'apppassword') {
265
+ if (!site.auth.username) {
266
+ throw new Error(`Site "${key}" (apppassword): requires auth.username`);
267
+ }
268
+ if (!site.auth.password_ref) {
269
+ throw new Error(`Site "${key}" (apppassword): requires auth.password_ref`);
270
+ }
271
+ if (!site.transport) {
272
+ throw new Error(`Site "${key}" (apppassword): missing "transport" (ssh or http)`);
273
+ }
274
+ if (site.transport === 'ssh') {
275
+ if (!site.ssh || !site.ssh.host || !site.ssh.path) {
276
+ throw new Error(`Site "${key}" (apppassword/ssh): requires ssh.host and ssh.path`);
277
+ }
278
+ } else if (site.transport === 'http') {
279
+ if (!site.http || !site.http.endpoint) {
280
+ throw new Error(`Site "${key}" (apppassword/http): requires http.endpoint`);
281
+ }
282
+ if (!site.http.endpoint.startsWith('https://') && !site.allowInsecure) {
283
+ throw new Error(`Site "${key}" (apppassword/http): endpoint is not HTTPS. Set "allowInsecure": true on the site to allow HTTP`);
284
+ }
285
+ } else {
286
+ throw new Error(`Site "${key}" (apppassword): unknown transport "${site.transport}" (use ssh or http)`);
287
+ }
288
+ return;
289
+ }
290
+
92
291
  if (!site.transport) {
93
292
  throw new Error(`Site "${key}": missing "transport" (ssh or http)`);
94
293
  }
@@ -119,6 +318,8 @@ function buildLegacyConfig(args) {
119
318
  return {
120
319
  defaultSite: 'default',
121
320
  _isMultiSite: false,
321
+ _configSource: 'legacy-cli',
322
+ _configSourceLabel: args.host,
122
323
  sites: {
123
324
  default: {
124
325
  label: args.host,
@@ -137,6 +338,7 @@ function buildLegacyConfig(args) {
137
338
 
138
339
  /**
139
340
  * Resolve a composite site key like "wicked.community" into config + subsite URL.
341
+ * Pure in-memory dispatch — kept synchronous since it has no I/O.
140
342
  */
141
343
  function resolveSiteKey(config, compositeKey) {
142
344
  // Direct match — "helena" or "wicked"
@@ -189,17 +391,23 @@ function buildSiteKeyEnum(config) {
189
391
  }
190
392
 
191
393
  /**
192
- * Resolve the password for an HTTP transport config, supporting
193
- * plaintext password, environment variable, or shell command.
394
+ * Resolve the password for an HTTP transport config, supporting plaintext
395
+ * password, environment variable, or shell command.
396
+ *
397
+ * `passwordCommand` is dispatched through `util.promisify(exec)` so the shell
398
+ * interprets the command string the same way the previous `execSync` did
399
+ * (operators rely on pipes, redirects, and command chaining — e.g.
400
+ * `op read 'op://Vault/foo' | tr -d '\n'` — which `execFile` cannot run).
194
401
  */
195
- function resolvePassword(httpConfig) {
402
+ async function resolvePassword(httpConfig) {
196
403
  if (httpConfig.passwordEnv) {
197
404
  const val = process.env[httpConfig.passwordEnv];
198
405
  if (!val) throw new Error(`Environment variable ${httpConfig.passwordEnv} is not set`);
199
406
  return val;
200
407
  }
201
408
  if (httpConfig.passwordCommand) {
202
- return execSync(httpConfig.passwordCommand, { encoding: 'utf8' }).trim();
409
+ const { stdout } = await execAsync(httpConfig.passwordCommand, { encoding: 'utf8' });
410
+ return stdout.trim();
203
411
  }
204
412
  if (httpConfig.password) {
205
413
  return httpConfig.password;
@@ -207,4 +415,56 @@ function resolvePassword(httpConfig) {
207
415
  throw new Error('No password, passwordEnv, or passwordCommand configured');
208
416
  }
209
417
 
210
- module.exports = { loadConfig, resolvePassword, resolveSiteKey, buildSiteKeyEnum };
418
+ /**
419
+ * Resolve a site's HTTP password, dispatching on schema shape.
420
+ *
421
+ * v2 App-Password sites (auth.method === 'apppassword' + auth.password_ref)
422
+ * read the secret from the keychain via the SecretStore. v1 carriers without
423
+ * a ref fall back to the `resolvePassword(http)` resolver.
424
+ *
425
+ * @param {object} site
426
+ * @param {object} [secretStore] SecretStore instance. Lazily defaults to
427
+ * KeychainSecretStore when an apppassword path
428
+ * runs and no store was supplied — preserves
429
+ * the "no keytar for SSH-only / v1-only setups"
430
+ * property by deferring the require to the
431
+ * point of need.
432
+ * @returns {Promise<string>}
433
+ */
434
+ async function resolveSitePassword(site, secretStore) {
435
+ if (site && site.auth && site.auth.method === 'apppassword' && site.auth.password_ref) {
436
+ let store = secretStore;
437
+ if (!store) {
438
+ const { KeychainSecretStore } = require('./auth/keychain-secret-store');
439
+ store = new KeychainSecretStore();
440
+ }
441
+ const { resolveRef } = require('./auth/secret-store');
442
+ return resolveRef(store, site.auth.password_ref);
443
+ }
444
+ if (site && site.http) {
445
+ return resolvePassword(site.http);
446
+ }
447
+ throw new Error('No password source configured for site');
448
+ }
449
+
450
+ /**
451
+ * Lazy wrapper around `lib/cli/config-store.js#seedFromEnvIfMissing`. Only
452
+ * loads the CLI config-store + KeychainSecretStore modules when the env-var
453
+ * branch of `loadConfig` actually fires — so SSH-only, explicit-config, and
454
+ * legacy-CLI install paths never pay the keytar import cost.
455
+ */
456
+ async function _seedFromEnvIfMissing(configPath, env) {
457
+ const { seedFromEnvIfMissing } = require('./cli/config-store');
458
+ return seedFromEnvIfMissing(configPath, env);
459
+ }
460
+
461
+ module.exports = {
462
+ loadConfig,
463
+ resolveConfigFilePath,
464
+ resolvePassword,
465
+ resolveSitePassword,
466
+ resolveSiteKey,
467
+ buildSiteKeyEnum,
468
+ buildEnvConfig,
469
+ validateSiteConfig,
470
+ };