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