@hostinger/mcp 1.52.0

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 (75) hide show
  1. package/.env.example +14 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +36 -0
  3. package/.github/workflows/build-release.yaml +163 -0
  4. package/CODEOWNERS +1 -0
  5. package/Dockerfile +15 -0
  6. package/LICENSE +21 -0
  7. package/README.md +4343 -0
  8. package/build.js +60 -0
  9. package/gemini-extension.json +25 -0
  10. package/glama.json +7 -0
  11. package/package.json +59 -0
  12. package/server.json +29 -0
  13. package/skills/headless/SKILL.md +55 -0
  14. package/skills/headless/entry/bootstrap.mjs +109 -0
  15. package/skills/headless/entry/skill.md +79 -0
  16. package/skills/headless/references/DEPLOYMENT.md +34 -0
  17. package/skills/headless/references/SETUP.md +34 -0
  18. package/skills/headless/references/STORE.md +52 -0
  19. package/skills/headless/references/WORDPRESS.md +51 -0
  20. package/src/core/oauth-error.html +90 -0
  21. package/src/core/oauth-success.html +77 -0
  22. package/src/core/oauth.js +406 -0
  23. package/src/core/oauth.ts +374 -0
  24. package/src/core/runtime.js +2673 -0
  25. package/src/core/runtime.ts +2724 -0
  26. package/src/core/tools/agency-hosting.js +1535 -0
  27. package/src/core/tools/agency-hosting.ts +1550 -0
  28. package/src/core/tools/all.js +15361 -0
  29. package/src/core/tools/all.ts +15376 -0
  30. package/src/core/tools/billing.js +310 -0
  31. package/src/core/tools/billing.ts +325 -0
  32. package/src/core/tools/dns.js +393 -0
  33. package/src/core/tools/dns.ts +408 -0
  34. package/src/core/tools/domains.js +1435 -0
  35. package/src/core/tools/domains.ts +1450 -0
  36. package/src/core/tools/ecommerce.js +1426 -0
  37. package/src/core/tools/ecommerce.ts +1441 -0
  38. package/src/core/tools/horizons.js +84 -0
  39. package/src/core/tools/horizons.ts +99 -0
  40. package/src/core/tools/hosting.js +2479 -0
  41. package/src/core/tools/hosting.ts +2494 -0
  42. package/src/core/tools/mail.js +1641 -0
  43. package/src/core/tools/mail.ts +1656 -0
  44. package/src/core/tools/reach.js +2270 -0
  45. package/src/core/tools/reach.ts +2285 -0
  46. package/src/core/tools/vps.js +2248 -0
  47. package/src/core/tools/vps.ts +2263 -0
  48. package/src/core/tools/wordpress.js +1570 -0
  49. package/src/core/tools/wordpress.ts +1585 -0
  50. package/src/servers/agency-hosting.js +6 -0
  51. package/src/servers/agency-hosting.ts +6 -0
  52. package/src/servers/all.js +6 -0
  53. package/src/servers/all.ts +6 -0
  54. package/src/servers/billing.js +6 -0
  55. package/src/servers/billing.ts +6 -0
  56. package/src/servers/dns.js +6 -0
  57. package/src/servers/dns.ts +6 -0
  58. package/src/servers/domains.js +6 -0
  59. package/src/servers/domains.ts +6 -0
  60. package/src/servers/ecommerce.js +6 -0
  61. package/src/servers/ecommerce.ts +6 -0
  62. package/src/servers/horizons.js +6 -0
  63. package/src/servers/horizons.ts +6 -0
  64. package/src/servers/hosting.js +6 -0
  65. package/src/servers/hosting.ts +6 -0
  66. package/src/servers/mail.js +6 -0
  67. package/src/servers/mail.ts +6 -0
  68. package/src/servers/reach.js +6 -0
  69. package/src/servers/reach.ts +6 -0
  70. package/src/servers/vps.js +6 -0
  71. package/src/servers/vps.ts +6 -0
  72. package/src/servers/wordpress.js +6 -0
  73. package/src/servers/wordpress.ts +6 -0
  74. package/tsconfig.json +15 -0
  75. package/types.d.ts +8836 -0
@@ -0,0 +1,2673 @@
1
+
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import minimist from 'minimist';
6
+ import cors from "cors";
7
+ import express from "express";
8
+ import axios from "axios";
9
+ import { config as dotenvConfig } from "dotenv";
10
+ import {
11
+ ListToolsRequestSchema,
12
+ CallToolRequestSchema,
13
+ } from "@modelcontextprotocol/sdk/types.js";
14
+ import { OAuthProvider, getEnvToken } from "./oauth.js";
15
+ import * as tus from "tus-js-client";
16
+ import fs from "fs";
17
+ import path from "path";
18
+
19
+ // Load environment variables
20
+ dotenvConfig({ quiet: true });
21
+
22
+ const SECURITY_SCHEMES = {
23
+ "apiToken": {
24
+ "type": "http",
25
+ "description": "API Token authentication",
26
+ "scheme": "bearer"
27
+ }
28
+ };
29
+
30
+ /**
31
+ * MCP Server for Hostinger API
32
+ * Generated from OpenAPI spec version 1.46.1
33
+ */
34
+ class MCPServer {
35
+ constructor({ name, version, tools }) {
36
+ // Initialize class properties
37
+ this.name = name;
38
+ this.version = version;
39
+ this.toolList = tools;
40
+ this.server = null;
41
+ this.tools = new Map();
42
+ this.debug = process.env.DEBUG === "true";
43
+ this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
44
+ this.headers = this.parseHeaders(process.env.API_HEADERS || "");
45
+ this.oauth = new OAuthProvider();
46
+
47
+ // Initialize tools map - do this before creating server
48
+ this.initializeTools();
49
+
50
+ // Create MCP server with correct capabilities
51
+ this.server = new Server(
52
+ {
53
+ name: this.name,
54
+ version: this.version,
55
+ },
56
+ {
57
+ capabilities: {
58
+ tools: {}, // Enable tools capability
59
+ },
60
+ }
61
+ );
62
+
63
+ // Set up request handlers - don't log here
64
+ this.setupHandlers();
65
+ }
66
+
67
+ /**
68
+ * Parse headers from string
69
+ */
70
+ parseHeaders(headerStr) {
71
+ const headers = {};
72
+ if (headerStr) {
73
+ headerStr.split(",").forEach((header) => {
74
+ const [key, value] = header.split(":");
75
+ if (key && value) headers[key.trim()] = value.trim();
76
+ });
77
+ }
78
+
79
+ const extensionUa = String(process.env.USER_AGENT ?? "")
80
+ .replace(/\r|\n/g, "")
81
+ .trim();
82
+ const base = `hostinger-mcp-server/${this.version}`;
83
+ headers["User-Agent"] = extensionUa ? `${base} (${extensionUa})` : base;
84
+
85
+ return headers;
86
+ }
87
+
88
+ /**
89
+ * Resolve a bearer token. HOSTINGER_API_TOKEN / API_TOKEN env vars take
90
+ * precedence; otherwise the OAuth provider handles login/refresh transparently.
91
+ */
92
+ async getAuthToken() {
93
+ return await this.oauth.getAccessToken();
94
+ }
95
+
96
+ /**
97
+ * Initialize tools map from OpenAPI spec
98
+ * This runs before the server is connected, so don't log here
99
+ */
100
+ initializeTools() {
101
+ // Initialize each tool in the tools map
102
+ for (const tool of this.toolList) {
103
+ this.tools.set(tool.name, {
104
+ name: tool.name,
105
+ title: tool.title,
106
+ description: tool.description,
107
+ inputSchema: tool.inputSchema,
108
+ annotations: tool.annotations,
109
+ // Don't include security at the tool level
110
+ });
111
+ }
112
+
113
+ // Don't log here, we're not connected yet
114
+ console.error(`Initialized ${this.tools.size} tools`);
115
+ }
116
+
117
+ /**
118
+ * Set up request handlers
119
+ */
120
+ setupHandlers() {
121
+ // Capture the connecting MCP client's identity once the initialize handshake completes.
122
+ // The name is client-controlled input, so strip CR/LF to prevent header injection.
123
+ this.server.oninitialized = () => {
124
+ const clientName = String(this.server.getClientVersion()?.name ?? "")
125
+ .replace(/\r|\n/g, "")
126
+ .trim();
127
+ if (clientName) {
128
+ this.headers["X-MCP-Client"] = clientName;
129
+ }
130
+ };
131
+
132
+ // Handle tool listing requests
133
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
134
+ this.log('debug', "Handling ListTools request");
135
+ // Return tools in the format expected by MCP SDK
136
+ return {
137
+ tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
138
+ id,
139
+ ...tool,
140
+ })),
141
+ };
142
+ });
143
+
144
+ // Handle tool execution requests
145
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
146
+ const { name, arguments: params } = request.params;
147
+ this.log('debug', "Handling CallTool request", { name, params });
148
+
149
+ let toolName;
150
+ let toolDetails;
151
+
152
+ // Find the requested tool
153
+ for (const [tid, tool] of this.tools.entries()) {
154
+ if (tool.name === name) {
155
+ toolName = name;
156
+ break;
157
+ }
158
+ }
159
+
160
+ if (!toolName) {
161
+ throw new Error(`Tool not found: ${name}`);
162
+ }
163
+
164
+ toolDetails = this.toolList.find(t => t.name === toolName);
165
+ if (!toolDetails) {
166
+ throw new Error(`Tool details not found for ID: ${toolName}`);
167
+ }
168
+
169
+ try {
170
+ this.log('info', `Executing tool: ${toolName}`);
171
+
172
+ let result;
173
+
174
+ if (toolDetails.custom) {
175
+ result = await this.executeCustomTool(toolDetails, params || {});
176
+ } else {
177
+ result = await this.executeApiCall(toolDetails, params || {});
178
+ }
179
+
180
+ // Return the result in the correct MCP format
181
+ return {
182
+ content: [
183
+ {
184
+ type: "text",
185
+ text: JSON.stringify(result)
186
+ }
187
+ ]
188
+ };
189
+
190
+ } catch (error) {
191
+ const errorMessage = error instanceof Error ? error.message : String(error);
192
+ const response = error.response;
193
+ this.log('error', `Error executing tool ${name}: ${errorMessage}`);
194
+
195
+ throw error;
196
+ }
197
+ });
198
+ }
199
+
200
+ async executeCustomTool(tool, params) {
201
+ switch (tool.name) {
202
+ case 'agency-hosting_deployNodeStaticWebsite':
203
+ return await this.handleNodeStaticDeploy(params);
204
+ case 'agency-hosting_deployPhpApplication':
205
+ return await this.handlePhpAppDeploy(params);
206
+ case 'hosting_importWordpressWebsite':
207
+ return await this.handleWordpressWebsiteImport(params);
208
+ case 'hosting_deployWordpressPlugin':
209
+ return await this.handleWordpressPluginDeploy(params);
210
+ case 'hosting_deployWordpressTheme':
211
+ return await this.handleWordpressThemeDeploy(params);
212
+ case 'hosting_deployJsApplication':
213
+ return await this.handleJavascriptApplicationDeploy(params);
214
+ case 'hosting_deployStaticWebsite':
215
+ return await this.handleStaticWebsiteDeploy(params);
216
+ case 'hosting_listJsDeployments':
217
+ return await this.handleListJavascriptDeployments(params);
218
+ case 'hosting_showJsDeploymentLogs':
219
+ return await this.handleShowJsDeploymentLogs(params);
220
+ default:
221
+ throw new Error(`Unknown custom tool: ${tool.name}`);
222
+ }
223
+ }
224
+
225
+ agencyHosting_normalizePath(pathString) {
226
+ return pathString.replace(/\\/g, '/');
227
+ }
228
+
229
+ agencyHosting_generateUploadDir() {
230
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
231
+ let result = '';
232
+ for (let i = 0; i < 12; i++) {
233
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
234
+ }
235
+ return result;
236
+ }
237
+
238
+ agencyHosting_validateRequiredParams(params) {
239
+ const { domain, archivePath, removeArchive } = params;
240
+
241
+ if (!domain || typeof domain !== 'string') {
242
+ throw new Error('domain is required and must be a string');
243
+ }
244
+
245
+ if (!archivePath || typeof archivePath !== 'string') {
246
+ throw new Error('archivePath is required and must be a string');
247
+ }
248
+
249
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
250
+ throw new Error('removeArchive must be a boolean if provided');
251
+ }
252
+ }
253
+
254
+ agencyHosting_validateArchiveFile(archivePath) {
255
+ if (!fs.existsSync(archivePath)) {
256
+ throw new Error(`Archive file not found: ${archivePath}`);
257
+ }
258
+
259
+ const archiveStats = fs.statSync(archivePath);
260
+ if (!archiveStats.isFile()) {
261
+ throw new Error(`Archive path is not a file: ${archivePath}`);
262
+ }
263
+ }
264
+
265
+ agencyHosting_removeArchive(archivePath, removeArchive) {
266
+ if (!removeArchive) {
267
+ return false;
268
+ }
269
+
270
+ try {
271
+ this.log('info', `Removing archive file: ${archivePath}`);
272
+ fs.unlinkSync(archivePath);
273
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
274
+ return true;
275
+ } catch (error) {
276
+ const errorMessage = error instanceof Error ? error.message : String(error);
277
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
278
+ return false;
279
+ }
280
+ }
281
+
282
+ async agencyHosting_resolveWebsiteUid(domain) {
283
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
284
+ const perPage = 100;
285
+
286
+ try {
287
+ const bearerToken = await this.getAuthToken();
288
+ let page = 1;
289
+ let total = 0;
290
+
291
+ do {
292
+ const url = new URL(
293
+ `api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
294
+ baseUrl
295
+ ).toString();
296
+
297
+ const config = {
298
+ method: 'get',
299
+ url,
300
+ headers: {
301
+ ...this.headers,
302
+ 'Authorization': `Bearer ${bearerToken}`
303
+ },
304
+ timeout: 60000, // 60s
305
+ validateStatus: function (status) {
306
+ return status < 500;
307
+ }
308
+ };
309
+
310
+ const response = await axios(config);
311
+
312
+ if (response.status !== 200) {
313
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
314
+ }
315
+
316
+ const domains = response.data?.data ?? [];
317
+ const match = domains.find((item) => item.fqdn === domain);
318
+ if (match?.website_uid) {
319
+ this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
320
+ return match.website_uid;
321
+ }
322
+
323
+ total = response.data?.meta?.total ?? domains.length;
324
+ page++;
325
+ } while ((page - 1) * perPage < total);
326
+
327
+ throw new Error(`No Agency Plan website found for domain: ${domain}`);
328
+
329
+ } catch (error) {
330
+ const errorMessage = error instanceof Error ? error.message : String(error);
331
+ this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
332
+
333
+ if (axios.isAxiosError(error)) {
334
+ const responseData = error.response?.data;
335
+ const responseStatus = error.response?.status;
336
+ this.log('error', 'API Error Details:', {
337
+ status: responseStatus,
338
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
339
+ });
340
+ }
341
+
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ async agencyHosting_fetchUploadCredentials(websiteUid) {
347
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
348
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
349
+
350
+ try {
351
+ const bearerToken = await this.getAuthToken();
352
+
353
+ const config = {
354
+ method: 'post',
355
+ url,
356
+ headers: {
357
+ ...this.headers,
358
+ 'Authorization': `Bearer ${bearerToken}`,
359
+ 'Content-Type': 'application/json'
360
+ },
361
+ timeout: 60000, // 60s
362
+ validateStatus: function (status) {
363
+ return status < 500;
364
+ }
365
+ };
366
+
367
+ const response = await axios(config);
368
+
369
+ if (response.status !== 200) {
370
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
371
+ }
372
+
373
+ return response.data;
374
+
375
+ } catch (error) {
376
+ const errorMessage = error instanceof Error ? error.message : String(error);
377
+ this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
378
+
379
+ if (axios.isAxiosError(error)) {
380
+ const responseData = error.response?.data;
381
+ const responseStatus = error.response?.status;
382
+ this.log('error', 'API Error Details:', {
383
+ status: responseStatus,
384
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
385
+ });
386
+ }
387
+
388
+ throw error;
389
+ }
390
+ }
391
+
392
+ async agencyHosting_uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
393
+ return new Promise(async (resolve, reject) => {
394
+ try {
395
+ const stats = fs.statSync(filePath);
396
+ const fileStream = fs.createReadStream(filePath);
397
+
398
+ const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
399
+ const normalizedPath = this.agencyHosting_normalizePath(relativePath);
400
+ const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
401
+
402
+ const requestHeaders = {
403
+ 'X-Auth': authToken,
404
+ 'X-Auth-Rest': authRestToken,
405
+ 'upload-length': stats.size.toString(),
406
+ 'upload-offset': '0'
407
+ };
408
+
409
+ try {
410
+ this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
411
+ await axios.post(uploadUrlWithFile, '', {
412
+ headers: requestHeaders,
413
+ timeout: 60000, // 60s
414
+ validateStatus: function (status) {
415
+ return status == 201;
416
+ }
417
+ });
418
+ } catch (error) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+
421
+ if (axios.isAxiosError(error)) {
422
+ const responseData = error.response?.data;
423
+ const responseStatus = error.response?.status;
424
+ const responseHeaders = error.response?.headers;
425
+ const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
426
+
427
+ this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
428
+ status: responseStatus,
429
+ headers: responseHeaders,
430
+ data: responseText,
431
+ message: errorMessage
432
+ });
433
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
434
+ return;
435
+ } else {
436
+ this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
437
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
438
+ return;
439
+ }
440
+ }
441
+
442
+ const upload = new tus.Upload(fileStream, {
443
+ uploadUrl: uploadUrlWithFile,
444
+ retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
445
+ uploadDataDuringCreation: false,
446
+ parallelUploads: 1,
447
+ chunkSize: 10485760,
448
+ headers: requestHeaders,
449
+ removeFingerprintOnSuccess: true,
450
+ uploadSize: stats.size,
451
+ metadata: {
452
+ filename: path.basename(relativePath)
453
+ },
454
+ onError: (error) => {
455
+ this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
456
+ reject(new Error(`Upload failed: ${error.message}`));
457
+ },
458
+ onSuccess: () => {
459
+ this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
460
+ resolve({
461
+ url: upload.url || uploadUrlWithFile,
462
+ filename: relativePath
463
+ });
464
+ }
465
+ });
466
+
467
+ upload.start();
468
+
469
+ } catch (error) {
470
+ const errorMessage = error instanceof Error ? error.message : String(error);
471
+ this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
472
+ reject(new Error(`Failed to prepare upload: ${errorMessage}`));
473
+ }
474
+ });
475
+ }
476
+
477
+ async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, archivePath) {
478
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
479
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
480
+
481
+ try {
482
+ const bearerToken = await this.getAuthToken();
483
+
484
+ const config = {
485
+ method: 'post',
486
+ url,
487
+ headers: {
488
+ ...this.headers,
489
+ 'Authorization': `Bearer ${bearerToken}`,
490
+ 'Content-Type': 'application/json'
491
+ },
492
+ data: {
493
+ archive_path: archivePath
494
+ },
495
+ timeout: 60000,
496
+ validateStatus: function (status) {
497
+ return status < 500;
498
+ }
499
+ };
500
+
501
+ const response = await axios(config);
502
+
503
+ if (response.status < 200 || response.status >= 300) {
504
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
505
+ }
506
+
507
+ this.log('info', `Successfully triggered build for website ${websiteUid}`);
508
+ return response.data;
509
+
510
+ } catch (error) {
511
+ const errorMessage = error instanceof Error ? error.message : String(error);
512
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
513
+
514
+ if (axios.isAxiosError(error)) {
515
+ const responseData = error.response?.data;
516
+ const responseStatus = error.response?.status;
517
+ this.log('error', 'API Error Details:', {
518
+ status: responseStatus,
519
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
520
+ });
521
+ }
522
+
523
+ throw error;
524
+ }
525
+ }
526
+
527
+ async handleNodeStaticDeploy(params) {
528
+ const { domain, archivePath, removeArchive = true } = params;
529
+
530
+ this.agencyHosting_validateRequiredParams(params);
531
+ this.agencyHosting_validateArchiveFile(archivePath);
532
+
533
+ this.log('info', `Resolving website UID from domain: ${domain}`);
534
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
535
+
536
+ this.log('info', `Starting archive upload for ${domain}`);
537
+
538
+ let uploadCredentials;
539
+ try {
540
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
541
+ } catch (error) {
542
+ const errorMessage = error instanceof Error ? error.message : String(error);
543
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
544
+ }
545
+
546
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
547
+
548
+ if (!uploadUrl || !authToken || !authRestToken) {
549
+ throw new Error('Invalid upload credentials received from API');
550
+ }
551
+
552
+ const archiveBasename = path.basename(archivePath);
553
+ const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
554
+ let uploadResult;
555
+ try {
556
+ uploadResult = await this.agencyHosting_uploadFile(
557
+ archivePath,
558
+ `${uploadDir}/${archiveBasename}`,
559
+ uploadUrl,
560
+ authRestToken,
561
+ authToken
562
+ );
563
+
564
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
565
+ } catch (error) {
566
+ const errorMessage = error instanceof Error ? error.message : String(error);
567
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
568
+ }
569
+
570
+ let deployResult;
571
+ try {
572
+ this.log('info', `Triggering build for ${domain}`);
573
+ deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
574
+ } catch (error) {
575
+ const errorMessage = error instanceof Error ? error.message : String(error);
576
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
577
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
578
+
579
+ return {
580
+ upload: {
581
+ status: 'success',
582
+ data: {
583
+ filename: uploadResult.filename
584
+ }
585
+ },
586
+ deploy: {
587
+ status: 'error',
588
+ error: errorMessage
589
+ },
590
+ removeArchive: {
591
+ status: archiveRemoved ? 'success' : 'skipped'
592
+ }
593
+ };
594
+ }
595
+
596
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
597
+
598
+ return {
599
+ upload: {
600
+ status: 'success',
601
+ data: {
602
+ filename: uploadResult.filename
603
+ }
604
+ },
605
+ deploy: {
606
+ status: 'success',
607
+ data: deployResult
608
+ },
609
+ removeArchive: {
610
+ status: archiveRemoved ? 'success' : 'skipped'
611
+ }
612
+ };
613
+ }
614
+
615
+ async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveName) {
616
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
617
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
618
+
619
+ try {
620
+ const bearerToken = await this.getAuthToken();
621
+
622
+ const config = {
623
+ method: 'post',
624
+ url,
625
+ headers: {
626
+ ...this.headers,
627
+ 'Authorization': `Bearer ${bearerToken}`,
628
+ 'Content-Type': 'application/json'
629
+ },
630
+ data: {
631
+ archive_name: archiveName
632
+ },
633
+ timeout: 60000,
634
+ validateStatus: function (status) {
635
+ return status < 500;
636
+ }
637
+ };
638
+
639
+ const response = await axios(config);
640
+
641
+ if (response.status < 200 || response.status >= 300) {
642
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
643
+ }
644
+
645
+ this.log('info', `Successfully triggered import for website ${websiteUid}`);
646
+ return response.data;
647
+
648
+ } catch (error) {
649
+ const errorMessage = error instanceof Error ? error.message : String(error);
650
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
651
+
652
+ if (axios.isAxiosError(error)) {
653
+ const responseData = error.response?.data;
654
+ const responseStatus = error.response?.status;
655
+ this.log('error', 'API Error Details:', {
656
+ status: responseStatus,
657
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
658
+ });
659
+ }
660
+
661
+ throw error;
662
+ }
663
+ }
664
+
665
+ async handlePhpAppDeploy(params) {
666
+ const { domain, archivePath, removeArchive = true } = params;
667
+
668
+ this.agencyHosting_validateRequiredParams(params);
669
+ this.agencyHosting_validateArchiveFile(archivePath);
670
+
671
+ this.log('info', `Resolving website UID from domain: ${domain}`);
672
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
673
+
674
+ this.log('info', `Starting archive upload for ${domain}`);
675
+
676
+ let uploadCredentials;
677
+ try {
678
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
679
+ } catch (error) {
680
+ const errorMessage = error instanceof Error ? error.message : String(error);
681
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
682
+ }
683
+
684
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
685
+
686
+ if (!uploadUrl || !authToken || !authRestToken) {
687
+ throw new Error('Invalid upload credentials received from API');
688
+ }
689
+
690
+ const archiveBasename = path.basename(archivePath);
691
+ let uploadResult;
692
+ try {
693
+ uploadResult = await this.agencyHosting_uploadFile(
694
+ archivePath,
695
+ `.h5g/${archiveBasename}`,
696
+ uploadUrl,
697
+ authRestToken,
698
+ authToken
699
+ );
700
+
701
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
702
+ } catch (error) {
703
+ const errorMessage = error instanceof Error ? error.message : String(error);
704
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
705
+ }
706
+
707
+ let deployResult;
708
+ try {
709
+ this.log('info', `Triggering import for ${domain}`);
710
+ deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
711
+ } catch (error) {
712
+ const errorMessage = error instanceof Error ? error.message : String(error);
713
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
714
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
715
+
716
+ return {
717
+ upload: {
718
+ status: 'success',
719
+ data: {
720
+ filename: uploadResult.filename
721
+ }
722
+ },
723
+ deploy: {
724
+ status: 'error',
725
+ error: errorMessage
726
+ },
727
+ removeArchive: {
728
+ status: archiveRemoved ? 'success' : 'skipped'
729
+ }
730
+ };
731
+ }
732
+
733
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
734
+
735
+ return {
736
+ upload: {
737
+ status: 'success',
738
+ data: {
739
+ filename: uploadResult.filename
740
+ }
741
+ },
742
+ deploy: {
743
+ status: 'success',
744
+ data: deployResult
745
+ },
746
+ removeArchive: {
747
+ status: archiveRemoved ? 'success' : 'skipped'
748
+ }
749
+ };
750
+ }
751
+
752
+ normalizePath(pathString) {
753
+ return pathString.replace(/\\/g, '/');
754
+ }
755
+
756
+ async resolveUsername(domain) {
757
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
758
+ const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
759
+
760
+ try {
761
+ const bearerToken = await this.getAuthToken();
762
+
763
+ const config = {
764
+ method: 'get',
765
+ url,
766
+ headers: {
767
+ ...this.headers,
768
+ 'Authorization': `Bearer ${bearerToken}`
769
+ },
770
+ timeout: 60000, // 60s
771
+ validateStatus: function (status) {
772
+ return status < 500;
773
+ }
774
+ };
775
+
776
+ const response = await axios(config);
777
+
778
+ if (response.status !== 200) {
779
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
780
+ }
781
+
782
+ const websites = response.data?.data;
783
+ if (!websites || websites.length === 0) {
784
+ throw new Error(`No website found for domain: ${domain}`);
785
+ }
786
+
787
+ const username = websites[0].username;
788
+ if (!username) {
789
+ throw new Error(`Username not found in website data for domain: ${domain}`);
790
+ }
791
+
792
+ this.log('info', `Resolved username: ${username} for domain: ${domain}`);
793
+ return username;
794
+
795
+ } catch (error) {
796
+ const errorMessage = error instanceof Error ? error.message : String(error);
797
+ this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
798
+
799
+ if (axios.isAxiosError(error)) {
800
+ const responseData = error.response?.data;
801
+ const responseStatus = error.response?.status;
802
+ this.log('error', 'API Error Details:', {
803
+ status: responseStatus,
804
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
805
+ });
806
+ }
807
+
808
+ throw error;
809
+ }
810
+ }
811
+
812
+ async fetchUploadCredentials(username, domain) {
813
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
814
+ const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
815
+
816
+ try {
817
+ const bearerToken = await this.getAuthToken();
818
+
819
+ const config = {
820
+ method: 'post',
821
+ url,
822
+ headers: {
823
+ ...this.headers,
824
+ 'Authorization': `Bearer ${bearerToken}`,
825
+ 'Content-Type': 'application/json'
826
+ },
827
+ data: {
828
+ username,
829
+ domain
830
+ },
831
+ timeout: 60000, // 60s
832
+ validateStatus: function (status) {
833
+ return status < 500;
834
+ }
835
+ };
836
+
837
+ const response = await axios(config);
838
+
839
+ if (response.status !== 200) {
840
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
841
+ }
842
+
843
+ return response.data;
844
+
845
+ } catch (error) {
846
+ const errorMessage = error instanceof Error ? error.message : String(error);
847
+ this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
848
+
849
+ if (axios.isAxiosError(error)) {
850
+ const responseData = error.response?.data;
851
+ const responseStatus = error.response?.status;
852
+ this.log('error', 'API Error Details:', {
853
+ status: responseStatus,
854
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
855
+ });
856
+ }
857
+
858
+ throw error;
859
+ }
860
+ }
861
+
862
+ async uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
863
+ return new Promise(async (resolve, reject) => {
864
+ try {
865
+ const stats = fs.statSync(filePath);
866
+ const fileStream = fs.createReadStream(filePath);
867
+
868
+ const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
869
+ const normalizedPath = this.normalizePath(relativePath);
870
+ const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
871
+
872
+ const requestHeaders = {
873
+ 'X-Auth': authToken,
874
+ 'X-Auth-Rest': authRestToken,
875
+ 'upload-length': stats.size.toString(),
876
+ 'upload-offset': '0'
877
+ };
878
+
879
+ try {
880
+ this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
881
+ await axios.post(uploadUrlWithFile, '', {
882
+ headers: requestHeaders,
883
+ timeout: 60000, // 60s
884
+ validateStatus: function (status) {
885
+ return status == 201;
886
+ }
887
+ });
888
+ } catch (error) {
889
+ const errorMessage = error instanceof Error ? error.message : String(error);
890
+
891
+ if (axios.isAxiosError(error)) {
892
+ const responseData = error.response?.data;
893
+ const responseStatus = error.response?.status;
894
+ const responseHeaders = error.response?.headers;
895
+ const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
896
+
897
+ this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
898
+ status: responseStatus,
899
+ headers: responseHeaders,
900
+ data: responseText,
901
+ message: errorMessage
902
+ });
903
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
904
+ return;
905
+ } else {
906
+ this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
907
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
908
+ return;
909
+ }
910
+ }
911
+
912
+ const upload = new tus.Upload(fileStream, {
913
+ uploadUrl: uploadUrlWithFile,
914
+ retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
915
+ uploadDataDuringCreation: false,
916
+ parallelUploads: 1,
917
+ chunkSize: 10485760,
918
+ headers: requestHeaders,
919
+ removeFingerprintOnSuccess: true,
920
+ uploadSize: stats.size,
921
+ metadata: {
922
+ filename: path.basename(relativePath)
923
+ },
924
+ onError: (error) => {
925
+ this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
926
+ reject(new Error(`Upload failed: ${error.message}`));
927
+ },
928
+ onSuccess: () => {
929
+ this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
930
+ resolve({
931
+ url: upload.url,
932
+ filename: relativePath
933
+ });
934
+ }
935
+ });
936
+
937
+ upload.start();
938
+
939
+ } catch (error) {
940
+ const errorMessage = error instanceof Error ? error.message : String(error);
941
+ this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
942
+ reject(new Error(`Failed to prepare upload: ${errorMessage}`));
943
+ }
944
+ });
945
+ }
946
+
947
+ hosting_importWordpressWebsite_validateArchiveFormat(filePath) {
948
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
949
+ const fileName = path.basename(filePath).toLowerCase();
950
+
951
+ for (const ext of validExtensions) {
952
+ if (fileName.endsWith(`.${ext}`)) {
953
+ return true;
954
+ }
955
+ }
956
+
957
+ return false;
958
+ }
959
+
960
+ hosting_importWordpressWebsite_validateRequiredParams(params) {
961
+ const { domain, archivePath, databaseDump } = params;
962
+
963
+ if (!domain || typeof domain !== 'string') {
964
+ throw new Error('domain is required and must be a string');
965
+ }
966
+
967
+ if (!archivePath || typeof archivePath !== 'string') {
968
+ throw new Error('archivePath is required and must be a string');
969
+ }
970
+
971
+ if (!databaseDump || typeof databaseDump !== 'string') {
972
+ throw new Error('databaseDump is required and must be a string');
973
+ }
974
+ }
975
+
976
+ hosting_importWordpressWebsite_validateArchiveFile(archivePath) {
977
+ if (!fs.existsSync(archivePath)) {
978
+ throw new Error(`Archive file not found: ${archivePath}`);
979
+ }
980
+
981
+ const archiveStats = fs.statSync(archivePath);
982
+ if (!archiveStats.isFile()) {
983
+ throw new Error(`Archive path is not a file: ${archivePath}`);
984
+ }
985
+
986
+ if (!this.hosting_importWordpressWebsite_validateArchiveFormat(archivePath)) {
987
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
988
+ }
989
+ }
990
+
991
+ hosting_importWordpressWebsite_validateDatabaseFile(databaseDump) {
992
+ if (!fs.existsSync(databaseDump)) {
993
+ throw new Error(`Database dump file not found: ${databaseDump}`);
994
+ }
995
+
996
+ const dbStats = fs.statSync(databaseDump);
997
+ if (!dbStats.isFile()) {
998
+ throw new Error(`Database dump path is not a file: ${databaseDump}`);
999
+ }
1000
+
1001
+ if (!databaseDump.toLowerCase().endsWith('.sql')) {
1002
+ throw new Error('Database dump must be a .sql file');
1003
+ }
1004
+ }
1005
+
1006
+ async hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain) {
1007
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1008
+ const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
1009
+
1010
+ try {
1011
+ const bearerToken = await this.getAuthToken();
1012
+
1013
+ const config = {
1014
+ method: 'get',
1015
+ url,
1016
+ headers: {
1017
+ ...this.headers,
1018
+ 'Authorization': `Bearer ${bearerToken}`
1019
+ },
1020
+ timeout: 60000, // 60s
1021
+ validateStatus: function (status) {
1022
+ return status < 500;
1023
+ }
1024
+ };
1025
+
1026
+ const response = await axios(config);
1027
+
1028
+ if (response.status !== 200) {
1029
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1030
+ }
1031
+
1032
+ const { is_empty } = response.data;
1033
+
1034
+ if (!is_empty) {
1035
+ throw new Error('Website is not empty. WordPress import can only be performed on empty sites. Please visit hPanel (https://hpanel.hostinger.com) and remove all existing files from the website before attempting to import.');
1036
+ }
1037
+
1038
+ this.log('info', `Website ${domain} is empty, proceeding with import`);
1039
+ return true;
1040
+
1041
+ } catch (error) {
1042
+ const errorMessage = error instanceof Error ? error.message : String(error);
1043
+ this.log('error', `Failed to check if website is empty: ${errorMessage}`);
1044
+
1045
+ if (axios.isAxiosError(error)) {
1046
+ const responseData = error.response?.data;
1047
+ const responseStatus = error.response?.status;
1048
+ this.log('error', 'API Error Details:', {
1049
+ status: responseStatus,
1050
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1051
+ });
1052
+ }
1053
+
1054
+ throw error;
1055
+ }
1056
+ }
1057
+
1058
+ async hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump) {
1059
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1060
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
1061
+
1062
+ try {
1063
+ const bearerToken = await this.getAuthToken();
1064
+
1065
+ const config = {
1066
+ method: 'post',
1067
+ url,
1068
+ headers: {
1069
+ ...this.headers,
1070
+ 'Authorization': `Bearer ${bearerToken}`,
1071
+ 'Content-Type': 'application/json'
1072
+ },
1073
+ data: {
1074
+ archive_path: path.basename(archivePath),
1075
+ sql_path: path.basename(databaseDump)
1076
+ },
1077
+ timeout: 60000, // 60s
1078
+ validateStatus: function (status) {
1079
+ return status < 500;
1080
+ }
1081
+ };
1082
+
1083
+ const response = await axios(config);
1084
+
1085
+ this.log('info', `Successfully triggered file extraction for ${domain}`);
1086
+ return true;
1087
+
1088
+ } catch (error) {
1089
+ const errorMessage = error instanceof Error ? error.message : String(error);
1090
+ this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
1091
+
1092
+ if (axios.isAxiosError(error)) {
1093
+ const responseData = error.response?.data;
1094
+ const responseStatus = error.response?.status;
1095
+ this.log('error', 'API Error Details:', {
1096
+ status: responseStatus,
1097
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1098
+ });
1099
+ }
1100
+
1101
+ throw error;
1102
+ }
1103
+ }
1104
+
1105
+ async handleWordpressWebsiteImport(params) {
1106
+ const { domain, archivePath, databaseDump } = params;
1107
+
1108
+ this.hosting_importWordpressWebsite_validateRequiredParams(params);
1109
+ this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
1110
+ this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
1111
+
1112
+ // Auto-resolve username from domain
1113
+ this.log('info', `Resolving username from domain: ${domain}`);
1114
+ const username = await this.resolveUsername(domain);
1115
+
1116
+ await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
1117
+
1118
+ const filesToUpload = [{
1119
+ absolutePath: archivePath,
1120
+ relativePath: path.basename(archivePath),
1121
+ type: 'archive'
1122
+ }, {
1123
+ absolutePath: databaseDump,
1124
+ relativePath: path.basename(databaseDump),
1125
+ type: 'database'
1126
+ }];
1127
+
1128
+ let uploadCredentials;
1129
+ try {
1130
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1131
+ } catch (error) {
1132
+ const errorMessage = error instanceof Error ? error.message : String(error);
1133
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1134
+ }
1135
+
1136
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1137
+
1138
+ if (!uploadUrl || !authToken || !authRestToken) {
1139
+ throw new Error('Invalid upload credentials received from API');
1140
+ }
1141
+
1142
+ this.log('info', `Starting website archive import to ${uploadUrl}`);
1143
+
1144
+ const results = [];
1145
+ let successCount = 0;
1146
+ let failureCount = 0;
1147
+
1148
+ for (const fileInfo of filesToUpload) {
1149
+ try {
1150
+ this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
1151
+
1152
+ const stats = fs.statSync(fileInfo.absolutePath);
1153
+ const uploadResult = await this.uploadFile(
1154
+ fileInfo.absolutePath,
1155
+ fileInfo.relativePath,
1156
+ uploadUrl,
1157
+ authRestToken,
1158
+ authToken
1159
+ );
1160
+
1161
+ results.push({
1162
+ file: fileInfo.absolutePath,
1163
+ remotePath: fileInfo.relativePath,
1164
+ type: fileInfo.type,
1165
+ status: 'success',
1166
+ uploadUrl: uploadResult.url,
1167
+ size: stats.size
1168
+ });
1169
+
1170
+ successCount++;
1171
+ this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
1172
+
1173
+ } catch (error) {
1174
+ const errorMessage = error instanceof Error ? error.message : String(error);
1175
+ results.push({
1176
+ file: fileInfo.absolutePath,
1177
+ remotePath: fileInfo.relativePath,
1178
+ type: fileInfo.type,
1179
+ status: 'error',
1180
+ error: errorMessage
1181
+ });
1182
+
1183
+ failureCount++;
1184
+ this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
1185
+ }
1186
+ }
1187
+
1188
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1189
+
1190
+ if (failureCount === 0) {
1191
+ try {
1192
+ this.log('info', 'All files uploaded successfully, triggering extraction...');
1193
+ await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
1194
+ } catch (error) {
1195
+ const errorMessage = error instanceof Error ? error.message : String(error);
1196
+ this.log('error', `File extraction failed: ${errorMessage}`);
1197
+ return {
1198
+ status: 'partial',
1199
+ summary: {
1200
+ total: filesToUpload.length,
1201
+ successful: successCount,
1202
+ failed: failureCount
1203
+ },
1204
+ results,
1205
+ extractionError: errorMessage
1206
+ };
1207
+ }
1208
+ }
1209
+
1210
+ return {
1211
+ status: overallStatus,
1212
+ summary: {
1213
+ total: filesToUpload.length,
1214
+ successful: successCount,
1215
+ failed: failureCount
1216
+ },
1217
+ results
1218
+ };
1219
+ }
1220
+
1221
+ hosting_deployWordpressPlugin_generateRandomString(length) {
1222
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1223
+ let result = '';
1224
+ for (let i = 0; i < length; i++) {
1225
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1226
+ }
1227
+ return result;
1228
+ }
1229
+
1230
+ hosting_deployWordpressPlugin_validateRequiredParams(params) {
1231
+ const { domain, slug, pluginPath } = params;
1232
+
1233
+ if (!domain || typeof domain !== 'string') {
1234
+ throw new Error('domain is required and must be a string');
1235
+ }
1236
+
1237
+ if (!slug || typeof slug !== 'string') {
1238
+ throw new Error('slug is required and must be a string');
1239
+ }
1240
+
1241
+ if (!pluginPath || typeof pluginPath !== 'string') {
1242
+ throw new Error('pluginPath is required and must be a string');
1243
+ }
1244
+ }
1245
+
1246
+ hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
1247
+ if (!fs.existsSync(pluginPath)) {
1248
+ throw new Error(`Plugin directory not found: ${pluginPath}`);
1249
+ }
1250
+
1251
+ const pluginStats = fs.statSync(pluginPath);
1252
+ if (!pluginStats.isDirectory()) {
1253
+ throw new Error(`Plugin path is not a directory: ${pluginPath}`);
1254
+ }
1255
+ }
1256
+
1257
+ hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
1258
+ const files = [];
1259
+
1260
+ const scanDir = (currentPath) => {
1261
+ const items = fs.readdirSync(currentPath);
1262
+
1263
+ for (const item of items) {
1264
+ const itemPath = path.join(currentPath, item);
1265
+ const stats = fs.statSync(itemPath);
1266
+
1267
+ if (stats.isDirectory()) {
1268
+ scanDir(itemPath);
1269
+ } else if (stats.isFile()) {
1270
+ const relativePath = path.relative(basePath, itemPath);
1271
+ files.push({
1272
+ absolutePath: itemPath,
1273
+ relativePath: relativePath
1274
+ });
1275
+ }
1276
+ }
1277
+ };
1278
+
1279
+ scanDir(dirPath);
1280
+ return files;
1281
+ }
1282
+
1283
+ async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
1284
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1285
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
1286
+
1287
+ try {
1288
+ const bearerToken = await this.getAuthToken();
1289
+
1290
+ const config = {
1291
+ method: 'post',
1292
+ url,
1293
+ headers: {
1294
+ ...this.headers,
1295
+ 'Authorization': `Bearer ${bearerToken}`,
1296
+ 'Content-Type': 'application/json'
1297
+ },
1298
+ data: {
1299
+ slug,
1300
+ plugin_path: pluginPath
1301
+ },
1302
+ timeout: 60000, // 60s
1303
+ validateStatus: function (status) {
1304
+ return status < 500;
1305
+ }
1306
+ };
1307
+
1308
+ const response = await axios(config);
1309
+
1310
+ if (response.status !== 200) {
1311
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1312
+ }
1313
+
1314
+ this.log('info', `Successfully triggered plugin deployment for ${domain}`);
1315
+ return true;
1316
+
1317
+ } catch (error) {
1318
+ const errorMessage = error instanceof Error ? error.message : String(error);
1319
+ this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
1320
+
1321
+ if (axios.isAxiosError(error)) {
1322
+ const responseData = error.response?.data;
1323
+ const responseStatus = error.response?.status;
1324
+ this.log('error', 'API Error Details:', {
1325
+ status: responseStatus,
1326
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1327
+ });
1328
+ }
1329
+
1330
+ throw error;
1331
+ }
1332
+ }
1333
+
1334
+ async handleWordpressPluginDeploy(params) {
1335
+ const { domain, slug, pluginPath } = params;
1336
+
1337
+ this.hosting_deployWordpressPlugin_validateRequiredParams(params);
1338
+ this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
1339
+
1340
+ this.log('info', `Resolving username from domain: ${domain}`);
1341
+ const username = await this.resolveUsername(domain);
1342
+
1343
+ const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
1344
+ const uploadDirName = `${slug}-${randomSuffix}`;
1345
+
1346
+ this.log('info', `Scanning plugin directory: ${pluginPath}`);
1347
+ const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
1348
+
1349
+ if (pluginFiles.length === 0) {
1350
+ throw new Error(`No files found in plugin directory: ${pluginPath}`);
1351
+ }
1352
+
1353
+ this.log('info', `Found ${pluginFiles.length} files to upload`);
1354
+
1355
+ let uploadCredentials;
1356
+ try {
1357
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1358
+ } catch (error) {
1359
+ const errorMessage = error instanceof Error ? error.message : String(error);
1360
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1361
+ }
1362
+
1363
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1364
+
1365
+ if (!uploadUrl || !authToken || !authRestToken) {
1366
+ throw new Error('Invalid upload credentials received from API');
1367
+ }
1368
+
1369
+ this.log('info', `Starting plugin file upload to ${uploadUrl}`);
1370
+
1371
+ const results = [];
1372
+ let successCount = 0;
1373
+ let failureCount = 0;
1374
+
1375
+ for (const fileInfo of pluginFiles) {
1376
+ try {
1377
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1378
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1379
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
1380
+
1381
+ const stats = fs.statSync(fileInfo.absolutePath);
1382
+ const uploadResult = await this.uploadFile(
1383
+ fileInfo.absolutePath,
1384
+ uploadPath,
1385
+ uploadUrl,
1386
+ authRestToken,
1387
+ authToken
1388
+ );
1389
+
1390
+ results.push({
1391
+ file: fileInfo.absolutePath,
1392
+ remotePath: uploadPath,
1393
+ status: 'success',
1394
+ uploadUrl: uploadResult.url,
1395
+ size: stats.size
1396
+ });
1397
+
1398
+ successCount++;
1399
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
1400
+
1401
+ } catch (error) {
1402
+ const errorMessage = error instanceof Error ? error.message : String(error);
1403
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1404
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1405
+
1406
+ results.push({
1407
+ file: fileInfo.absolutePath,
1408
+ remotePath: uploadPath,
1409
+ status: 'error',
1410
+ error: errorMessage
1411
+ });
1412
+
1413
+ failureCount++;
1414
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1415
+ }
1416
+ }
1417
+
1418
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1419
+
1420
+ if (failureCount === 0) {
1421
+ try {
1422
+ this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
1423
+ await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
1424
+ } catch (error) {
1425
+ const errorMessage = error instanceof Error ? error.message : String(error);
1426
+ this.log('error', `Plugin deployment failed: ${errorMessage}`);
1427
+ return {
1428
+ status: 'partial',
1429
+ summary: {
1430
+ total: pluginFiles.length,
1431
+ successful: successCount,
1432
+ failed: failureCount
1433
+ },
1434
+ results,
1435
+ deploymentError: errorMessage
1436
+ };
1437
+ }
1438
+ }
1439
+
1440
+ return {
1441
+ status: overallStatus,
1442
+ summary: {
1443
+ total: pluginFiles.length,
1444
+ successful: successCount,
1445
+ failed: failureCount
1446
+ },
1447
+ results,
1448
+ uploadDirName
1449
+ };
1450
+ }
1451
+
1452
+ hosting_deployWordpressTheme_generateRandomString(length) {
1453
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1454
+ let result = '';
1455
+ for (let i = 0; i < length; i++) {
1456
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1457
+ }
1458
+ return result;
1459
+ }
1460
+
1461
+ hosting_deployWordpressTheme_validateRequiredParams(params) {
1462
+ const { domain, slug, themePath } = params;
1463
+
1464
+ if (!domain || typeof domain !== 'string') {
1465
+ throw new Error('domain is required and must be a string');
1466
+ }
1467
+
1468
+ if (!slug || typeof slug !== 'string') {
1469
+ throw new Error('slug is required and must be a string');
1470
+ }
1471
+
1472
+ if (!themePath || typeof themePath !== 'string') {
1473
+ throw new Error('themePath is required and must be a string');
1474
+ }
1475
+ }
1476
+
1477
+ hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
1478
+ if (!fs.existsSync(themePath)) {
1479
+ throw new Error(`Theme directory not found: ${themePath}`);
1480
+ }
1481
+
1482
+ const themeStats = fs.statSync(themePath);
1483
+ if (!themeStats.isDirectory()) {
1484
+ throw new Error(`Theme path is not a directory: ${themePath}`);
1485
+ }
1486
+ }
1487
+
1488
+ hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
1489
+ const files = [];
1490
+
1491
+ const scanDir = (currentPath) => {
1492
+ const items = fs.readdirSync(currentPath);
1493
+
1494
+ for (const item of items) {
1495
+ const itemPath = path.join(currentPath, item);
1496
+ const stats = fs.statSync(itemPath);
1497
+
1498
+ if (stats.isDirectory()) {
1499
+ scanDir(itemPath);
1500
+ } else if (stats.isFile()) {
1501
+ const relativePath = path.relative(basePath, itemPath);
1502
+ files.push({
1503
+ absolutePath: itemPath,
1504
+ relativePath: relativePath
1505
+ });
1506
+ }
1507
+ }
1508
+ };
1509
+
1510
+ scanDir(dirPath);
1511
+ return files;
1512
+ }
1513
+
1514
+ async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
1515
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1516
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
1517
+
1518
+ try {
1519
+ const bearerToken = await this.getAuthToken();
1520
+
1521
+ const config = {
1522
+ method: 'post',
1523
+ url,
1524
+ headers: {
1525
+ ...this.headers,
1526
+ 'Authorization': `Bearer ${bearerToken}`,
1527
+ 'Content-Type': 'application/json'
1528
+ },
1529
+ data: {
1530
+ slug,
1531
+ theme_path: themePath,
1532
+ is_activated: activate
1533
+ },
1534
+ timeout: 60000, // 60s
1535
+ validateStatus: function (status) {
1536
+ return status < 500;
1537
+ }
1538
+ };
1539
+
1540
+ const response = await axios(config);
1541
+
1542
+ if (response.status !== 200) {
1543
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1544
+ }
1545
+
1546
+ this.log('info', `Successfully triggered theme deployment for ${domain}`);
1547
+ return true;
1548
+
1549
+ } catch (error) {
1550
+ const errorMessage = error instanceof Error ? error.message : String(error);
1551
+ this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
1552
+
1553
+ if (axios.isAxiosError(error)) {
1554
+ const responseData = error.response?.data;
1555
+ const responseStatus = error.response?.status;
1556
+ this.log('error', 'API Error Details:', {
1557
+ status: responseStatus,
1558
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1559
+ });
1560
+ }
1561
+
1562
+ throw error;
1563
+ }
1564
+ }
1565
+
1566
+ async handleWordpressThemeDeploy(params) {
1567
+ const { domain, slug, themePath, activate = false } = params;
1568
+
1569
+ this.hosting_deployWordpressTheme_validateRequiredParams(params);
1570
+ this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
1571
+
1572
+ this.log('info', `Resolving username from domain: ${domain}`);
1573
+ const username = await this.resolveUsername(domain);
1574
+
1575
+ const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
1576
+ const uploadDirName = `${slug}-${randomSuffix}`;
1577
+
1578
+ this.log('info', `Scanning theme directory: ${themePath}`);
1579
+ const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
1580
+
1581
+ if (themeFiles.length === 0) {
1582
+ throw new Error(`No files found in theme directory: ${themePath}`);
1583
+ }
1584
+
1585
+ this.log('info', `Found ${themeFiles.length} files to upload`);
1586
+
1587
+ let uploadCredentials;
1588
+ try {
1589
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1590
+ } catch (error) {
1591
+ const errorMessage = error instanceof Error ? error.message : String(error);
1592
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1593
+ }
1594
+
1595
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1596
+
1597
+ if (!uploadUrl || !authToken || !authRestToken) {
1598
+ throw new Error('Invalid upload credentials received from API');
1599
+ }
1600
+
1601
+ this.log('info', `Starting theme file upload to ${uploadUrl}`);
1602
+
1603
+ const results = [];
1604
+ let successCount = 0;
1605
+ let failureCount = 0;
1606
+
1607
+ for (const fileInfo of themeFiles) {
1608
+ try {
1609
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1610
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1611
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
1612
+
1613
+ const stats = fs.statSync(fileInfo.absolutePath);
1614
+ const uploadResult = await this.uploadFile(
1615
+ fileInfo.absolutePath,
1616
+ uploadPath,
1617
+ uploadUrl,
1618
+ authRestToken,
1619
+ authToken
1620
+ );
1621
+
1622
+ results.push({
1623
+ file: fileInfo.absolutePath,
1624
+ remotePath: uploadPath,
1625
+ status: 'success',
1626
+ uploadUrl: uploadResult.url,
1627
+ size: stats.size
1628
+ });
1629
+
1630
+ successCount++;
1631
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
1632
+
1633
+ } catch (error) {
1634
+ const errorMessage = error instanceof Error ? error.message : String(error);
1635
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1636
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1637
+
1638
+ results.push({
1639
+ file: fileInfo.absolutePath,
1640
+ remotePath: uploadPath,
1641
+ status: 'error',
1642
+ error: errorMessage
1643
+ });
1644
+
1645
+ failureCount++;
1646
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1647
+ }
1648
+ }
1649
+
1650
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1651
+
1652
+ if (failureCount === 0) {
1653
+ try {
1654
+ this.log('info', 'All files uploaded successfully, triggering theme deployment...');
1655
+ await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
1656
+ } catch (error) {
1657
+ const errorMessage = error instanceof Error ? error.message : String(error);
1658
+ this.log('error', `Theme deployment failed: ${errorMessage}`);
1659
+ return {
1660
+ status: 'partial',
1661
+ summary: {
1662
+ total: themeFiles.length,
1663
+ successful: successCount,
1664
+ failed: failureCount
1665
+ },
1666
+ results,
1667
+ deploymentError: errorMessage
1668
+ };
1669
+ }
1670
+ }
1671
+
1672
+ return {
1673
+ status: overallStatus,
1674
+ summary: {
1675
+ total: themeFiles.length,
1676
+ successful: successCount,
1677
+ failed: failureCount
1678
+ },
1679
+ results,
1680
+ uploadDirName,
1681
+ activated: activate
1682
+ };
1683
+ }
1684
+
1685
+ hosting_deployJsApplication_validateArchiveFormat(filePath) {
1686
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1687
+ const fileName = path.basename(filePath).toLowerCase();
1688
+
1689
+ for (const ext of validExtensions) {
1690
+ if (fileName.endsWith(`.${ext}`)) {
1691
+ return true;
1692
+ }
1693
+ }
1694
+
1695
+ return false;
1696
+ }
1697
+
1698
+ hosting_deployJsApplication_validateRequiredParams(params) {
1699
+ const { domain, archivePath, removeArchive } = params;
1700
+
1701
+ if (!domain || typeof domain !== 'string') {
1702
+ throw new Error('domain is required and must be a string');
1703
+ }
1704
+
1705
+ if (!archivePath || typeof archivePath !== 'string') {
1706
+ throw new Error('archivePath is required and must be a string');
1707
+ }
1708
+
1709
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
1710
+ throw new Error('removeArchive must be a boolean if provided');
1711
+ }
1712
+ }
1713
+
1714
+ hosting_deployJsApplication_removeArchive(archivePath, removeArchive) {
1715
+ if (!removeArchive) {
1716
+ return false;
1717
+ }
1718
+
1719
+ try {
1720
+ this.log('info', `Removing archive file: ${archivePath}`);
1721
+ fs.unlinkSync(archivePath);
1722
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
1723
+ return true;
1724
+ } catch (error) {
1725
+ const errorMessage = error instanceof Error ? error.message : String(error);
1726
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
1727
+ // Don't fail the entire operation if archive removal fails
1728
+ return false;
1729
+ }
1730
+ }
1731
+
1732
+ hosting_deployJsApplication_validateArchiveFile(archivePath) {
1733
+ if (!fs.existsSync(archivePath)) {
1734
+ throw new Error(`Archive file not found: ${archivePath}`);
1735
+ }
1736
+
1737
+ const archiveStats = fs.statSync(archivePath);
1738
+ if (!archiveStats.isFile()) {
1739
+ throw new Error(`Archive path is not a file: ${archivePath}`);
1740
+ }
1741
+
1742
+ if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
1743
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
1744
+ }
1745
+ }
1746
+
1747
+ async hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath) {
1748
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1749
+ const archiveBasename = path.basename(archivePath);
1750
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
1751
+
1752
+ try {
1753
+ const bearerToken = await this.getAuthToken();
1754
+
1755
+ const config = {
1756
+ method: 'get',
1757
+ url,
1758
+ headers: {
1759
+ ...this.headers,
1760
+ 'Authorization': `Bearer ${bearerToken}`
1761
+ },
1762
+ timeout: 60000, // 60s
1763
+ validateStatus: function (status) {
1764
+ return status < 500;
1765
+ }
1766
+ };
1767
+
1768
+ const response = await axios(config);
1769
+
1770
+ if (response.status !== 200) {
1771
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1772
+ }
1773
+
1774
+ this.log('info', `Successfully fetched build settings for ${domain}`);
1775
+ return response.data;
1776
+
1777
+ } catch (error) {
1778
+ const errorMessage = error instanceof Error ? error.message : String(error);
1779
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
1780
+
1781
+ if (axios.isAxiosError(error)) {
1782
+ const responseData = error.response?.data;
1783
+ const responseStatus = error.response?.status;
1784
+ this.log('error', 'API Error Details:', {
1785
+ status: responseStatus,
1786
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1787
+ });
1788
+ }
1789
+
1790
+ throw error;
1791
+ }
1792
+ }
1793
+
1794
+ async hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings) {
1795
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1796
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1797
+
1798
+ try {
1799
+ const bearerToken = await this.getAuthToken();
1800
+
1801
+ const archiveBasename = path.basename(archivePath);
1802
+ const buildData = {
1803
+ ...buildSettings,
1804
+ node_version: buildSettings?.node_version || 20,
1805
+ source_type: 'archive',
1806
+ source_options: {
1807
+ archive_path: archiveBasename
1808
+ }
1809
+ };
1810
+
1811
+ const config = {
1812
+ method: 'post',
1813
+ url,
1814
+ headers: {
1815
+ ...this.headers,
1816
+ 'Authorization': `Bearer ${bearerToken}`,
1817
+ 'Content-Type': 'application/json'
1818
+ },
1819
+ data: buildData,
1820
+ timeout: 60000, // 60s
1821
+ validateStatus: function (status) {
1822
+ return status < 500;
1823
+ }
1824
+ };
1825
+
1826
+ const response = await axios(config);
1827
+
1828
+ if (response.status !== 200) {
1829
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1830
+ }
1831
+
1832
+ this.log('info', `Successfully triggered build for ${domain}`);
1833
+ return response.data;
1834
+
1835
+ } catch (error) {
1836
+ const errorMessage = error instanceof Error ? error.message : String(error);
1837
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
1838
+
1839
+ if (axios.isAxiosError(error)) {
1840
+ const responseData = error.response?.data;
1841
+ const responseStatus = error.response?.status;
1842
+ this.log('error', 'API Error Details:', {
1843
+ status: responseStatus,
1844
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1845
+ });
1846
+ }
1847
+
1848
+ throw error;
1849
+ }
1850
+ }
1851
+
1852
+ async handleJavascriptApplicationDeploy(params) {
1853
+ const { domain, archivePath, removeArchive = false } = params;
1854
+
1855
+ this.hosting_deployJsApplication_validateRequiredParams(params);
1856
+ this.hosting_deployJsApplication_validateArchiveFile(archivePath);
1857
+
1858
+ // Auto-resolve username from domain
1859
+ this.log('info', `Resolving username from domain: ${domain}`);
1860
+ const username = await this.resolveUsername(domain);
1861
+
1862
+ // Upload archive file
1863
+ this.log('info', `Starting archive upload for ${domain}`);
1864
+
1865
+ let uploadCredentials;
1866
+ try {
1867
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1868
+ } catch (error) {
1869
+ const errorMessage = error instanceof Error ? error.message : String(error);
1870
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1871
+ }
1872
+
1873
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1874
+
1875
+ if (!uploadUrl || !authToken || !authRestToken) {
1876
+ throw new Error('Invalid upload credentials received from API');
1877
+ }
1878
+
1879
+ const archiveBasename = path.basename(archivePath);
1880
+ let uploadResult;
1881
+ try {
1882
+ const stats = fs.statSync(archivePath);
1883
+ uploadResult = await this.uploadFile(
1884
+ archivePath,
1885
+ archiveBasename,
1886
+ uploadUrl,
1887
+ authRestToken,
1888
+ authToken
1889
+ );
1890
+
1891
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
1892
+ } catch (error) {
1893
+ const errorMessage = error instanceof Error ? error.message : String(error);
1894
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
1895
+ }
1896
+
1897
+ // Fetch build settings
1898
+ let buildSettings;
1899
+ try {
1900
+ this.log('info', `Fetching build settings for ${domain}`);
1901
+ buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
1902
+ } catch (error) {
1903
+ const errorMessage = error instanceof Error ? error.message : String(error);
1904
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
1905
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1906
+
1907
+ return {
1908
+ upload: {
1909
+ status: 'success',
1910
+ data: {
1911
+ filename: uploadResult.filename
1912
+ }
1913
+ },
1914
+ resolveSettings: {
1915
+ status: 'error',
1916
+ error: errorMessage
1917
+ },
1918
+ build: {
1919
+ status: 'skipped'
1920
+ },
1921
+ removeArchive: {
1922
+ status: archiveRemoved ? 'success' : 'skipped'
1923
+ }
1924
+ };
1925
+ }
1926
+
1927
+ // Trigger build
1928
+ let buildResult;
1929
+ try {
1930
+ this.log('info', `Triggering build for ${domain}`);
1931
+ buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
1932
+ } catch (error) {
1933
+ const errorMessage = error instanceof Error ? error.message : String(error);
1934
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
1935
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1936
+
1937
+ return {
1938
+ upload: {
1939
+ status: 'success',
1940
+ data: {
1941
+ filename: uploadResult.filename
1942
+ }
1943
+ },
1944
+ resolveSettings: {
1945
+ status: 'success',
1946
+ data: buildSettings
1947
+ },
1948
+ build: {
1949
+ status: 'error',
1950
+ error: errorMessage
1951
+ },
1952
+ removeArchive: {
1953
+ status: archiveRemoved ? 'success' : 'skipped'
1954
+ }
1955
+ };
1956
+ }
1957
+
1958
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1959
+
1960
+ return {
1961
+ upload: {
1962
+ status: 'success',
1963
+ data: {
1964
+ filename: uploadResult.filename
1965
+ }
1966
+ },
1967
+ resolveSettings: {
1968
+ status: 'success',
1969
+ data: buildSettings
1970
+ },
1971
+ build: {
1972
+ status: 'success',
1973
+ data: buildResult
1974
+ },
1975
+ removeArchive: {
1976
+ status: archiveRemoved ? 'success' : 'skipped'
1977
+ }
1978
+ };
1979
+ }
1980
+
1981
+ hosting_deployStaticWebsite_validateArchiveFormat(filePath) {
1982
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1983
+ const fileName = path.basename(filePath).toLowerCase();
1984
+
1985
+ for (const ext of validExtensions) {
1986
+ if (fileName.endsWith(`.${ext}`)) {
1987
+ return true;
1988
+ }
1989
+ }
1990
+
1991
+ return false;
1992
+ }
1993
+
1994
+ hosting_deployStaticWebsite_validateRequiredParams(params) {
1995
+ const { domain, archivePath, removeArchive } = params;
1996
+
1997
+ if (!domain || typeof domain !== 'string') {
1998
+ throw new Error('domain is required and must be a string');
1999
+ }
2000
+
2001
+ if (!archivePath || typeof archivePath !== 'string') {
2002
+ throw new Error('archivePath is required and must be a string');
2003
+ }
2004
+
2005
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
2006
+ throw new Error('removeArchive must be a boolean if provided');
2007
+ }
2008
+ }
2009
+
2010
+ hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive) {
2011
+ if (!removeArchive) {
2012
+ return false;
2013
+ }
2014
+
2015
+ try {
2016
+ this.log('info', `Removing archive file: ${archivePath}`);
2017
+ fs.unlinkSync(archivePath);
2018
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
2019
+ return true;
2020
+ } catch (error) {
2021
+ const errorMessage = error instanceof Error ? error.message : String(error);
2022
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
2023
+ return false;
2024
+ }
2025
+ }
2026
+
2027
+ hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
2028
+ if (!fs.existsSync(archivePath)) {
2029
+ throw new Error(`Archive file not found: ${archivePath}`);
2030
+ }
2031
+
2032
+ const archiveStats = fs.statSync(archivePath);
2033
+ if (!archiveStats.isFile()) {
2034
+ throw new Error(`Archive path is not a file: ${archivePath}`);
2035
+ }
2036
+
2037
+ if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
2038
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
2039
+ }
2040
+ }
2041
+
2042
+ async hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath) {
2043
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2044
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
2045
+
2046
+ try {
2047
+ const bearerToken = await this.getAuthToken();
2048
+
2049
+ const archiveBasename = path.basename(archivePath);
2050
+ const deployData = {
2051
+ archive_path: archiveBasename
2052
+ };
2053
+
2054
+ const config = {
2055
+ method: 'post',
2056
+ url,
2057
+ headers: {
2058
+ ...this.headers,
2059
+ 'Authorization': `Bearer ${bearerToken}`,
2060
+ 'Content-Type': 'application/json'
2061
+ },
2062
+ data: deployData,
2063
+ timeout: 60000,
2064
+ validateStatus: function (status) {
2065
+ return status < 500;
2066
+ }
2067
+ };
2068
+
2069
+ const response = await axios(config);
2070
+
2071
+ if (response.status !== 200) {
2072
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2073
+ }
2074
+
2075
+ this.log('info', `Successfully triggered deployment for ${domain}`);
2076
+ return response.data;
2077
+
2078
+ } catch (error) {
2079
+ const errorMessage = error instanceof Error ? error.message : String(error);
2080
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
2081
+
2082
+ if (axios.isAxiosError(error)) {
2083
+ const responseData = error.response?.data;
2084
+ const responseStatus = error.response?.status;
2085
+ this.log('error', 'API Error Details:', {
2086
+ status: responseStatus,
2087
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2088
+ });
2089
+ }
2090
+
2091
+ throw error;
2092
+ }
2093
+ }
2094
+
2095
+ async handleStaticWebsiteDeploy(params) {
2096
+ const { domain, archivePath, removeArchive = false } = params;
2097
+
2098
+ this.hosting_deployStaticWebsite_validateRequiredParams(params);
2099
+ this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
2100
+
2101
+ this.log('info', `Resolving username from domain: ${domain}`);
2102
+ const username = await this.resolveUsername(domain);
2103
+
2104
+ this.log('info', `Starting archive upload for ${domain}`);
2105
+
2106
+ let uploadCredentials;
2107
+ try {
2108
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
2109
+ } catch (error) {
2110
+ const errorMessage = error instanceof Error ? error.message : String(error);
2111
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
2112
+ }
2113
+
2114
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
2115
+
2116
+ if (!uploadUrl || !authToken || !authRestToken) {
2117
+ throw new Error('Invalid upload credentials received from API');
2118
+ }
2119
+
2120
+ const archiveBasename = path.basename(archivePath);
2121
+ let uploadResult;
2122
+ try {
2123
+ const stats = fs.statSync(archivePath);
2124
+ uploadResult = await this.uploadFile(
2125
+ archivePath,
2126
+ archiveBasename,
2127
+ uploadUrl,
2128
+ authRestToken,
2129
+ authToken
2130
+ );
2131
+
2132
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
2133
+ } catch (error) {
2134
+ const errorMessage = error instanceof Error ? error.message : String(error);
2135
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
2136
+ }
2137
+
2138
+ let deployResult;
2139
+ try {
2140
+ this.log('info', `Triggering deployment for ${domain}`);
2141
+ deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
2142
+ } catch (error) {
2143
+ const errorMessage = error instanceof Error ? error.message : String(error);
2144
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
2145
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
2146
+
2147
+ return {
2148
+ upload: {
2149
+ status: 'success',
2150
+ data: {
2151
+ filename: uploadResult.filename
2152
+ }
2153
+ },
2154
+ deploy: {
2155
+ status: 'error',
2156
+ error: errorMessage
2157
+ },
2158
+ removeArchive: {
2159
+ status: archiveRemoved ? 'success' : 'skipped'
2160
+ }
2161
+ };
2162
+ }
2163
+
2164
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
2165
+
2166
+ return {
2167
+ upload: {
2168
+ status: 'success',
2169
+ data: {
2170
+ filename: uploadResult.filename
2171
+ }
2172
+ },
2173
+ deploy: {
2174
+ status: 'success',
2175
+ data: deployResult
2176
+ },
2177
+ removeArchive: {
2178
+ status: archiveRemoved ? 'success' : 'skipped'
2179
+ }
2180
+ };
2181
+ }
2182
+
2183
+ hosting_listJsDeployments_validateRequiredParams(params) {
2184
+ const { domain } = params;
2185
+
2186
+ if (!domain || typeof domain !== 'string') {
2187
+ throw new Error('domain is required and must be a string');
2188
+ }
2189
+ }
2190
+
2191
+ hosting_listJsDeployments_buildQueryParams(params) {
2192
+ const { page, perPage, states } = params;
2193
+ const queryParams = new URLSearchParams();
2194
+
2195
+ if (page !== undefined && page !== null) {
2196
+ queryParams.append('page', page.toString());
2197
+ }
2198
+
2199
+ if (perPage !== undefined && perPage !== null) {
2200
+ queryParams.append('per_page', perPage.toString());
2201
+ }
2202
+
2203
+ if (states && Array.isArray(states) && states.length > 0) {
2204
+ states.forEach(state => {
2205
+ queryParams.append('states[]', state);
2206
+ });
2207
+ }
2208
+
2209
+ return queryParams.toString();
2210
+ }
2211
+
2212
+ async hosting_listJsDeployments_fetchDeployments(username, domain, queryParams) {
2213
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2214
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
2215
+
2216
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
2217
+
2218
+ try {
2219
+ const bearerToken = await this.getAuthToken();
2220
+
2221
+ const config = {
2222
+ method: 'get',
2223
+ url: fullUrl,
2224
+ headers: {
2225
+ ...this.headers,
2226
+ 'Authorization': `Bearer ${bearerToken}`
2227
+ },
2228
+ timeout: 60000, // 60s
2229
+ validateStatus: function (status) {
2230
+ return status < 500;
2231
+ }
2232
+ };
2233
+
2234
+ const response = await axios(config);
2235
+
2236
+ if (response.status !== 200) {
2237
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2238
+ }
2239
+
2240
+ this.log('info', `Successfully fetched deployments for ${domain}`);
2241
+ return response.data;
2242
+
2243
+ } catch (error) {
2244
+ const errorMessage = error instanceof Error ? error.message : String(error);
2245
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
2246
+
2247
+ if (axios.isAxiosError(error)) {
2248
+ const responseData = error.response?.data;
2249
+ const responseStatus = error.response?.status;
2250
+ this.log('error', 'API Error Details:', {
2251
+ status: responseStatus,
2252
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2253
+ });
2254
+ }
2255
+
2256
+ throw error;
2257
+ }
2258
+ }
2259
+
2260
+ async handleListJavascriptDeployments(params) {
2261
+ const { domain, page, perPage, states } = params;
2262
+
2263
+ this.hosting_listJsDeployments_validateRequiredParams(params);
2264
+
2265
+ // Auto-resolve username from domain
2266
+ this.log('info', `Resolving username from domain: ${domain}`);
2267
+ const username = await this.resolveUsername(domain);
2268
+
2269
+ // Build query parameters
2270
+ const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
2271
+
2272
+ // Fetch deployments
2273
+ let deployments;
2274
+ try {
2275
+ this.log('info', `Fetching deployments for ${domain}`);
2276
+ deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
2277
+ } catch (error) {
2278
+ const errorMessage = error instanceof Error ? error.message : String(error);
2279
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
2280
+ throw error;
2281
+ }
2282
+
2283
+ return {
2284
+ status: 'success',
2285
+ domain,
2286
+ username,
2287
+ queryParams: {
2288
+ page,
2289
+ perPage,
2290
+ states
2291
+ },
2292
+ deployments
2293
+ };
2294
+ }
2295
+
2296
+ hosting_showJsDeploymentLogs_validateRequiredParams(params) {
2297
+ const { domain, buildUuid, fromLine } = params;
2298
+
2299
+ if (!domain || typeof domain !== 'string') {
2300
+ throw new Error('domain is required and must be a string');
2301
+ }
2302
+
2303
+ if (!buildUuid || typeof buildUuid !== 'string') {
2304
+ throw new Error('buildUuid is required and must be a string');
2305
+ }
2306
+
2307
+ if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
2308
+ throw new Error('fromLine must be a non-negative integer when provided');
2309
+ }
2310
+ }
2311
+
2312
+ hosting_showJsDeploymentLogs_buildQueryParams(params) {
2313
+ const { fromLine } = params;
2314
+ const queryParams = new URLSearchParams();
2315
+
2316
+ const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
2317
+ queryParams.append('from_line', line.toString());
2318
+
2319
+ return queryParams.toString();
2320
+ }
2321
+
2322
+ async hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams) {
2323
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2324
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
2325
+
2326
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
2327
+
2328
+ try {
2329
+ const bearerToken = await this.getAuthToken();
2330
+
2331
+ const config = {
2332
+ method: 'get',
2333
+ url: fullUrl,
2334
+ headers: {
2335
+ ...this.headers,
2336
+ 'Authorization': `Bearer ${bearerToken}`
2337
+ },
2338
+ timeout: 60000,
2339
+ validateStatus: function (status) {
2340
+ return status < 500;
2341
+ }
2342
+ };
2343
+
2344
+ const response = await axios(config);
2345
+
2346
+ if (response.status !== 200) {
2347
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2348
+ }
2349
+
2350
+ this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
2351
+ return response.data;
2352
+
2353
+ } catch (error) {
2354
+ const errorMessage = error instanceof Error ? error.message : String(error);
2355
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
2356
+
2357
+ if (axios.isAxiosError(error)) {
2358
+ const responseData = error.response?.data;
2359
+ const responseStatus = error.response?.status;
2360
+ this.log('error', 'API Error Details:', {
2361
+ status: responseStatus,
2362
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2363
+ });
2364
+ }
2365
+
2366
+ throw error;
2367
+ }
2368
+ }
2369
+
2370
+ async handleShowJsDeploymentLogs(params) {
2371
+ const { domain, buildUuid, fromLine } = params;
2372
+
2373
+ this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
2374
+
2375
+ this.log('info', `Resolving username from domain: ${domain}`);
2376
+ const username = await this.resolveUsername(domain);
2377
+
2378
+ const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
2379
+
2380
+ let logs;
2381
+ try {
2382
+ this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
2383
+ logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
2384
+ } catch (error) {
2385
+ const errorMessage = error instanceof Error ? error.message : String(error);
2386
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
2387
+ throw error;
2388
+ }
2389
+
2390
+ const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
2391
+
2392
+ return {
2393
+ domain,
2394
+ username,
2395
+ buildUuid,
2396
+ fromLine: effectiveFromLine,
2397
+ logs
2398
+ };
2399
+ }
2400
+
2401
+ /**
2402
+ * Execute an API call for a tool
2403
+ */
2404
+ async executeApiCall(tool, params) {
2405
+ // Get method and path from tool
2406
+ const method = tool.method;
2407
+ let path = tool.path;
2408
+
2409
+ // Clone params to avoid modifying the original
2410
+ const requestParams = { ...params };
2411
+
2412
+ // Replace path parameters with values from params
2413
+ Object.entries(requestParams).forEach(([key, value]) => {
2414
+ const placeholder = `{${key}}`;
2415
+ if (path.includes(placeholder)) {
2416
+ path = path.replace(placeholder, encodeURIComponent(String(value)));
2417
+ delete requestParams[key]; // Remove used parameter
2418
+ }
2419
+ });
2420
+
2421
+ // Build the full URL
2422
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2423
+ const cleanPath = path.startsWith("/") ? path.slice(1) : path;
2424
+ const url = new URL(cleanPath, baseUrl).toString();
2425
+
2426
+ this.log('debug', `API Request: ${method} ${url}`);
2427
+
2428
+ try {
2429
+ // Configure the request
2430
+ const config = {
2431
+ method: method.toLowerCase(),
2432
+ url,
2433
+ headers: { ...this.headers },
2434
+ timeout: 60000, // 60s
2435
+ validateStatus: function (status) {
2436
+ return status < 500; // Resolve only if the status code is less than 500
2437
+ }
2438
+ };
2439
+
2440
+ const envToken = getEnvToken();
2441
+ let bearerToken = await this.getAuthToken();
2442
+ config.headers['Authorization'] = `Bearer ${bearerToken}`;
2443
+
2444
+ // Add parameters based on request method
2445
+ if (["GET", "DELETE"].includes(method)) {
2446
+ // For GET/DELETE, send params as query string
2447
+ config.params = { ...(config.params || {}), ...requestParams };
2448
+ } else {
2449
+ // For POST/PUT/PATCH, send params as JSON body
2450
+ config.data = requestParams;
2451
+ config.headers["Content-Type"] = "application/json";
2452
+ }
2453
+
2454
+ this.log('debug', "Request config:", {
2455
+ url: config.url,
2456
+ method: config.method,
2457
+ params: config.params,
2458
+ headers: Object.keys(config.headers)
2459
+ });
2460
+
2461
+ // Execute the request
2462
+ let response = await axios(config);
2463
+ this.log('debug', `Response status: ${response.status}`);
2464
+
2465
+ // Reactive token recovery: a 401 means the bearer was rejected even
2466
+ // though our local expiry said it was fine (e.g. revoked, account
2467
+ // changed, clock skew). Force a re-auth via refresh-or-login and retry
2468
+ // once. Skipped for the env-token path — nothing to refresh.
2469
+ if (response.status === 401 && !envToken) {
2470
+ this.log('info', 'API returned 401; reauthenticating and retrying once');
2471
+ bearerToken = await this.oauth.reauthenticate();
2472
+ config.headers['Authorization'] = `Bearer ${bearerToken}`;
2473
+ response = await axios(config);
2474
+ this.log('debug', `Retry response status: ${response.status}`);
2475
+ }
2476
+
2477
+ return response.data;
2478
+
2479
+ } catch (error) {
2480
+ const errorMessage = error instanceof Error ? error.message : String(error);
2481
+ this.log('error', `API request failed: ${errorMessage}`);
2482
+
2483
+ if (axios.isAxiosError(error)) {
2484
+ const responseData = error.response?.data;
2485
+ const responseStatus = error.response?.status;
2486
+
2487
+ this.log('error', 'API Error Details:', {
2488
+ status: responseStatus,
2489
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2490
+ });
2491
+
2492
+ // Rethrow with more context for better error handling
2493
+ const detailedError = new Error(`API request failed with status ${responseStatus}: ${errorMessage}`);
2494
+ detailedError.response = error.response;
2495
+ throw detailedError;
2496
+ }
2497
+
2498
+ throw error;
2499
+ }
2500
+ }
2501
+
2502
+ /**
2503
+ * Log messages with appropriate level
2504
+ * Only sends to MCP if we're connected
2505
+ */
2506
+ log(level, message, data) {
2507
+ // Debug logs (which may include request params) only reach stderr when
2508
+ // DEBUG=true; info/warning/error always log. This gate also guards the MCP send.
2509
+ if (this.debug || level !== 'debug') {
2510
+ console.error(`[${level.toUpperCase()}] ${message}${data ? ': ' + JSON.stringify(data) : ''}`);
2511
+ try {
2512
+ // Only send if server exists and is connected
2513
+ if (this.server && this.server.isConnected) {
2514
+ this.server.sendLoggingMessage({
2515
+ level,
2516
+ data: `[MCP Server] ${message}${data ? ': ' + JSON.stringify(data) : ''}`
2517
+ });
2518
+ }
2519
+ } catch (e) {
2520
+ // If logging fails, log to stderr
2521
+ console.error('Failed to send log via MCP:', e.message);
2522
+ }
2523
+ }
2524
+ }
2525
+
2526
+ /**
2527
+ * Create and configure Express app with shared middleware
2528
+ */
2529
+ createApp() {
2530
+ const app = express();
2531
+ app.use(express.json());
2532
+ app.use(cors());
2533
+ return app;
2534
+ }
2535
+
2536
+ /**
2537
+ * Start the server with HTTP streaming transport
2538
+ */
2539
+ async startHttp(host, port) {
2540
+ try {
2541
+ const app = this.createApp();
2542
+
2543
+ // Create HTTP transport with session management
2544
+ const httpTransport = new StreamableHTTPServerTransport({
2545
+ sessionIdGenerator: () => {
2546
+ // Generate a simple session ID
2547
+ return `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2548
+ },
2549
+ });
2550
+
2551
+ // Set up CORS for all routes
2552
+ app.options("/{*splat}", (req, res) => {
2553
+ res.header("Access-Control-Allow-Origin", "*");
2554
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2555
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
2556
+ res.sendStatus(200);
2557
+ });
2558
+
2559
+ // Health check endpoint
2560
+ app.get("/health", (req, res) => {
2561
+ res.status(200).json({ status: "ok", transport: "http" });
2562
+ });
2563
+
2564
+ // Set up the HTTP transport endpoint
2565
+ app.post("/", async (req, res) => {
2566
+ res.header("Access-Control-Allow-Origin", "*");
2567
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2568
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
2569
+ await httpTransport.handleRequest(req, res, req.body);
2570
+ });
2571
+
2572
+ // Start the server
2573
+ const server = app.listen(port, host, () => {
2574
+ this.log('info', `MCP Server with HTTP streaming transport started successfully with ${this.tools.size} tools`);
2575
+ this.log('info', `Listening on http://${host}:${port}`);
2576
+ });
2577
+
2578
+ // Connect the MCP server to the transport
2579
+ await this.server.connect(httpTransport);
2580
+
2581
+ } catch (error) {
2582
+ console.error("Failed to start MCP server:", error);
2583
+ process.exit(1);
2584
+ }
2585
+ }
2586
+
2587
+ /**
2588
+ * Start the server
2589
+ */
2590
+ async startStdio() {
2591
+ try {
2592
+ // Create stdio transport
2593
+ const transport = new StdioServerTransport();
2594
+ console.error("MCP Server starting on stdio transport");
2595
+
2596
+ // Connect to the transport
2597
+ await this.server.connect(transport);
2598
+
2599
+ // Now we can safely log via MCP
2600
+ console.error(`Registered ${this.tools.size} tools`);
2601
+ this.log('info', `MCP Server with stdio transport started successfully with ${this.tools.size} tools`);
2602
+ } catch (error) {
2603
+ console.error("Failed to start MCP server:", error);
2604
+ process.exit(1);
2605
+ }
2606
+ }
2607
+ }
2608
+
2609
+ export async function startServer({ name, version, tools }) {
2610
+ const argv = minimist(process.argv.slice(2), {
2611
+ string: ['host'],
2612
+ boolean: ['stdio', 'http', 'help', 'version', 'login', 'logout'],
2613
+ alias: { v: 'version' },
2614
+ default: { host: '127.0.0.1', port: 8100, stdio: true }
2615
+ });
2616
+
2617
+ if (argv.version) {
2618
+ console.log(version);
2619
+ process.exit(0);
2620
+ }
2621
+
2622
+ if (argv.help) {
2623
+ console.log(`
2624
+ ${name}
2625
+ Usage: ${name} [options]
2626
+ Options:
2627
+ --http Use HTTP streaming transport (requires HOSTINGER_API_TOKEN env var)
2628
+ --stdio Use standard input/output transport (default)
2629
+ --host <host> Host to bind to (default: 127.0.0.1)
2630
+ --port <port> Port to bind to (default: 8100)
2631
+ --login Run OAuth sign-in flow and exit
2632
+ --logout Revoke stored OAuth credentials and exit
2633
+ --version, -v Show version number
2634
+ --help Show this help message
2635
+ Environment Variables:
2636
+ HOSTINGER_API_TOKEN Hostinger API token (overrides OAuth when set)
2637
+ API_TOKEN Deprecated alias for HOSTINGER_API_TOKEN (will be removed in a future version)
2638
+ OAUTH_ISSUER OAuth server base URL (default: https://auth.hostinger.com)
2639
+ DEBUG Enable debug logging (true/false)
2640
+ `);
2641
+ process.exit(0);
2642
+ }
2643
+
2644
+ if (argv.login) {
2645
+ const provider = new OAuthProvider();
2646
+ console.error('[OAuth] Starting sign-in flow...');
2647
+ await provider.login();
2648
+ console.error('[OAuth] Sign-in successful. Credentials stored.');
2649
+ process.exit(0);
2650
+ }
2651
+
2652
+ if (argv.logout) {
2653
+ const provider = new OAuthProvider();
2654
+ await provider.logout();
2655
+ console.error('[OAuth] Signed out. Stored credentials revoked and cleared.');
2656
+ process.exit(0);
2657
+ }
2658
+
2659
+ if (argv.http) {
2660
+ const envToken = getEnvToken();
2661
+ if (!envToken) {
2662
+ console.error('[Error] HTTP transport requires the HOSTINGER_API_TOKEN environment variable. OAuth sign-in is only supported in stdio mode.');
2663
+ process.exit(1);
2664
+ }
2665
+ }
2666
+
2667
+ const server = new MCPServer({ name, version, tools });
2668
+ if (argv.http) {
2669
+ await server.startHttp(argv.host, argv.port);
2670
+ } else {
2671
+ await server.startStdio();
2672
+ }
2673
+ }