@ebowwa/hetzner 0.1.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 (46) hide show
  1. package/actions.js +802 -0
  2. package/actions.ts +1053 -0
  3. package/auth.js +35 -0
  4. package/auth.ts +37 -0
  5. package/bootstrap/FIREWALL.md +326 -0
  6. package/bootstrap/KERNEL-HARDENING.md +258 -0
  7. package/bootstrap/SECURITY-INTEGRATION.md +281 -0
  8. package/bootstrap/TESTING.md +301 -0
  9. package/bootstrap/cloud-init.js +279 -0
  10. package/bootstrap/cloud-init.ts +394 -0
  11. package/bootstrap/firewall.js +279 -0
  12. package/bootstrap/firewall.ts +342 -0
  13. package/bootstrap/genesis.js +406 -0
  14. package/bootstrap/genesis.ts +518 -0
  15. package/bootstrap/index.js +35 -0
  16. package/bootstrap/index.ts +71 -0
  17. package/bootstrap/kernel-hardening.js +266 -0
  18. package/bootstrap/kernel-hardening.test.ts +230 -0
  19. package/bootstrap/kernel-hardening.ts +272 -0
  20. package/bootstrap/security-audit.js +118 -0
  21. package/bootstrap/security-audit.ts +124 -0
  22. package/bootstrap/ssh-hardening.js +182 -0
  23. package/bootstrap/ssh-hardening.ts +192 -0
  24. package/client.js +137 -0
  25. package/client.ts +177 -0
  26. package/config.js +5 -0
  27. package/config.ts +5 -0
  28. package/errors.js +270 -0
  29. package/errors.ts +371 -0
  30. package/index.js +28 -0
  31. package/index.ts +55 -0
  32. package/package.json +56 -0
  33. package/pricing.js +284 -0
  34. package/pricing.ts +422 -0
  35. package/schemas.js +660 -0
  36. package/schemas.ts +765 -0
  37. package/server-status.ts +81 -0
  38. package/servers.js +424 -0
  39. package/servers.ts +568 -0
  40. package/ssh-keys.js +90 -0
  41. package/ssh-keys.ts +122 -0
  42. package/ssh-setup.ts +218 -0
  43. package/types.js +96 -0
  44. package/types.ts +389 -0
  45. package/volumes.js +172 -0
  46. package/volumes.ts +229 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Fetch server status from Hetzner API by IP address
3
+ * Used for network error detection to determine if server is actually running
4
+ */
5
+
6
+ import { HetznerClient } from "./client.js";
7
+
8
+ /**
9
+ * Cache for server status lookups
10
+ * Key: IP address, Value: { status, timestamp }
11
+ */
12
+ const statusCache = new Map<string, { status: string; timestamp: number }>();
13
+ const CACHE_TTL = 30 * 1000; // 30 seconds
14
+
15
+ /**
16
+ * Get server status by IP address
17
+ * @param ip - The server IP address
18
+ * @param client - HetznerClient instance
19
+ * @returns Server status ("running", "stopped", etc.) or null if not found
20
+ */
21
+ export async function getServerStatusByIP(
22
+ ip: string,
23
+ client?: HetznerClient
24
+ ): Promise<string | null> {
25
+ // Check cache first
26
+ const cached = statusCache.get(ip);
27
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
28
+ return cached.status;
29
+ }
30
+
31
+ // If no client provided, create one (will use env vars)
32
+ const hetznerClient = client || new HetznerClient();
33
+
34
+ try {
35
+ // List all servers and find matching IP
36
+ const servers = await hetznerClient.listServers();
37
+
38
+ // Find server with matching IPv4
39
+ const server = servers.find(
40
+ (s) => s.public_net.ipv4.ip === ip
41
+ );
42
+
43
+ if (server) {
44
+ const status = server.status;
45
+ // Cache the result
46
+ statusCache.set(ip, { status, timestamp: Date.now() });
47
+ return status;
48
+ }
49
+
50
+ // Server not found
51
+ return null;
52
+ } catch (error) {
53
+ // Log error but don't throw - we don't want to break SSH connection on API failure
54
+ console.warn(`[NetworkError] Failed to fetch server status for ${ip}:`, error);
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Clear the status cache
61
+ * Useful for testing or when you know server state has changed
62
+ */
63
+ export function clearServerStatusCache(): void {
64
+ statusCache.clear();
65
+ }
66
+
67
+ /**
68
+ * Get server status by IP with a fallback default
69
+ * @param ip - The server IP address
70
+ * @param client - HetznerClient instance (optional)
71
+ * @param defaultStatus - Default status to return if lookup fails (default: "unknown")
72
+ * @returns Server status or defaultStatus if not found/error
73
+ */
74
+ export async function getServerStatusByIPWithDefault(
75
+ ip: string,
76
+ client?: HetznerClient,
77
+ defaultStatus: string = "unknown"
78
+ ): Promise<string> {
79
+ const status = await getServerStatusByIP(ip, client);
80
+ return status ?? defaultStatus;
81
+ }
package/servers.js ADDED
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Hetzner server operations
3
+ */
4
+ import { z } from "zod";
5
+ import { HetznerListServersResponseSchema, HetznerGetServerResponseSchema, HetznerCreateServerResponseSchema, HetznerActionSchema, } from "./schemas.js";
6
+ export class ServerOperations {
7
+ client;
8
+ constructor(client) {
9
+ this.client = client;
10
+ }
11
+ /**
12
+ * List all servers
13
+ */
14
+ async list() {
15
+ const response = await this.client.request("/servers");
16
+ // Validate response with Zod
17
+ const validated = HetznerListServersResponseSchema.safeParse(response);
18
+ if (!validated.success) {
19
+ console.warn('Hetzner list servers validation warning:', validated.error.issues);
20
+ return response.servers; // Return unvalidated data for backward compatibility
21
+ }
22
+ return validated.data.servers;
23
+ }
24
+ /**
25
+ * Get a specific server by ID
26
+ */
27
+ async get(id) {
28
+ const response = await this.client.request(`/servers/${id}`);
29
+ // Validate response with Zod
30
+ const validated = HetznerGetServerResponseSchema.safeParse(response);
31
+ if (!validated.success) {
32
+ console.warn('Hetzner get server validation warning:', validated.error.issues);
33
+ return response.server; // Return unvalidated data for backward compatibility
34
+ }
35
+ return validated.data.server;
36
+ }
37
+ /**
38
+ * Create a new server
39
+ *
40
+ * @param options - Server creation options
41
+ * @returns Create server response including server, action, and next_actions
42
+ */
43
+ async create(options) {
44
+ // Validate input with Zod
45
+ const createServerOptionsSchema = z.object({
46
+ name: z.string().min(1),
47
+ server_type: z.string().min(1).default("cpx11"),
48
+ image: z.string().min(1).default("ubuntu-24.04"),
49
+ location: z.string().min(1).optional(),
50
+ datacenter: z.string().min(1).optional(),
51
+ ssh_keys: z.array(z.union([z.string(), z.number()])).default([]),
52
+ volumes: z.array(z.number()).default([]),
53
+ labels: z.record(z.string(), z.any()).optional(),
54
+ start_after_create: z.boolean().default(true),
55
+ user_data: z.string().optional(),
56
+ });
57
+ const validatedOptions = createServerOptionsSchema.safeParse(options);
58
+ if (!validatedOptions.success) {
59
+ throw new Error(`Invalid server options: ${validatedOptions.error.issues.map(i => i.message).join(', ')}`);
60
+ }
61
+ // Ensure either location or datacenter, not both
62
+ if (validatedOptions.data.location && validatedOptions.data.datacenter) {
63
+ throw new Error('Cannot specify both location and datacenter');
64
+ }
65
+ const body = {
66
+ name: validatedOptions.data.name,
67
+ server_type: validatedOptions.data.server_type,
68
+ image: validatedOptions.data.image,
69
+ ...(validatedOptions.data.location && { location: validatedOptions.data.location }),
70
+ ...(validatedOptions.data.datacenter && { datacenter: { id: validatedOptions.data.datacenter } }),
71
+ ssh_keys: validatedOptions.data.ssh_keys,
72
+ volumes: validatedOptions.data.volumes,
73
+ ...(validatedOptions.data.labels && { labels: validatedOptions.data.labels }),
74
+ start_after_create: validatedOptions.data.start_after_create,
75
+ ...(validatedOptions.data.user_data && { user_data: validatedOptions.data.user_data }),
76
+ };
77
+ console.log('[Hetzner] Creating server with body:', JSON.stringify(body, null, 2));
78
+ const response = await this.client.request("/servers", {
79
+ method: "POST",
80
+ body: JSON.stringify(body),
81
+ });
82
+ // Validate response with Zod
83
+ const validatedResponse = HetznerCreateServerResponseSchema.safeParse(response);
84
+ if (!validatedResponse.success) {
85
+ console.warn('Hetzner create server validation warning:', validatedResponse.error.issues);
86
+ return response; // Return unvalidated data for backward compatibility
87
+ }
88
+ return validatedResponse.data;
89
+ }
90
+ /**
91
+ * Create a new server and wait for it to be ready
92
+ *
93
+ * This convenience method creates a server and waits for the initial action to complete.
94
+ *
95
+ * @param options - Server creation options
96
+ * @param onProgress - Optional progress callback
97
+ * @returns Server once ready
98
+ */
99
+ async createAndWait(options, onProgress) {
100
+ const response = await this.create(options);
101
+ // Build wait options (only include onProgress if defined)
102
+ const waitOptions = onProgress !== undefined ? { onProgress } : {};
103
+ // Wait for the main create action to complete
104
+ if (response.action.status === 'running') {
105
+ await this.client.actions.waitFor(response.action.id, waitOptions);
106
+ }
107
+ // Wait for any next actions (e.g., start server)
108
+ if (response.next_actions.length > 0) {
109
+ for (const action of response.next_actions) {
110
+ await this.client.actions.waitFor(action.id, waitOptions);
111
+ }
112
+ }
113
+ // Return the server object
114
+ const server = await this.get(response.server.id);
115
+ return server;
116
+ }
117
+ /**
118
+ * Delete a server
119
+ *
120
+ * @param id - Server ID
121
+ * @returns Action for server deletion
122
+ */
123
+ async delete(id) {
124
+ // Validate ID with Zod
125
+ const serverIdSchema = z.number().int().positive();
126
+ const validatedId = serverIdSchema.safeParse(id);
127
+ if (!validatedId.success) {
128
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
129
+ }
130
+ const response = await this.client.request(`/servers/${validatedId.data}`, { method: "DELETE" });
131
+ const validated = HetznerActionSchema.safeParse(response.action);
132
+ if (!validated.success) {
133
+ console.warn('Hetzner delete server validation warning:', validated.error.issues);
134
+ return response.action;
135
+ }
136
+ return validated.data;
137
+ }
138
+ /**
139
+ * Delete a server and wait for completion
140
+ *
141
+ * @param id - Server ID
142
+ * @param onProgress - Optional progress callback
143
+ */
144
+ async deleteAndWait(id, onProgress) {
145
+ const action = await this.delete(id);
146
+ await this.client.actions.waitFor(action.id, { onProgress });
147
+ }
148
+ /**
149
+ * Power on a server
150
+ *
151
+ * @param id - Server ID
152
+ * @returns Action for server power on
153
+ */
154
+ async powerOn(id) {
155
+ // Validate ID with Zod
156
+ const serverIdSchema = z.number().int().positive();
157
+ const validatedId = serverIdSchema.safeParse(id);
158
+ if (!validatedId.success) {
159
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
160
+ }
161
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/poweron`, { method: "POST" });
162
+ const validated = HetznerActionSchema.safeParse(response.action);
163
+ if (!validated.success) {
164
+ console.warn('Hetzner power on validation warning:', validated.error.issues);
165
+ return response.action;
166
+ }
167
+ return validated.data;
168
+ }
169
+ /**
170
+ * Power on a server and wait for completion
171
+ *
172
+ * @param id - Server ID
173
+ * @param onProgress - Optional progress callback
174
+ */
175
+ async powerOnAndWait(id, onProgress) {
176
+ const action = await this.powerOn(id);
177
+ return await this.client.actions.waitFor(action.id, { onProgress });
178
+ }
179
+ /**
180
+ * Power off a server
181
+ *
182
+ * @param id - Server ID
183
+ * @returns Action for server power off
184
+ */
185
+ async powerOff(id) {
186
+ // Validate ID with Zod
187
+ const serverIdSchema = z.number().int().positive();
188
+ const validatedId = serverIdSchema.safeParse(id);
189
+ if (!validatedId.success) {
190
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
191
+ }
192
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/poweroff`, { method: "POST" });
193
+ const validated = HetznerActionSchema.safeParse(response.action);
194
+ if (!validated.success) {
195
+ console.warn('Hetzner power off validation warning:', validated.error.issues);
196
+ return response.action;
197
+ }
198
+ return validated.data;
199
+ }
200
+ /**
201
+ * Power off a server and wait for completion
202
+ *
203
+ * @param id - Server ID
204
+ * @param onProgress - Optional progress callback
205
+ */
206
+ async powerOffAndWait(id, onProgress) {
207
+ const action = await this.powerOff(id);
208
+ return await this.client.actions.waitFor(action.id, { onProgress });
209
+ }
210
+ /**
211
+ * Reboot a server
212
+ *
213
+ * @param id - Server ID
214
+ * @returns Action for server reboot
215
+ */
216
+ async reboot(id) {
217
+ // Validate ID with Zod
218
+ const serverIdSchema = z.number().int().positive();
219
+ const validatedId = serverIdSchema.safeParse(id);
220
+ if (!validatedId.success) {
221
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
222
+ }
223
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/reboot`, { method: "POST" });
224
+ const validated = HetznerActionSchema.safeParse(response.action);
225
+ if (!validated.success) {
226
+ console.warn('Hetzner reboot validation warning:', validated.error.issues);
227
+ return response.action;
228
+ }
229
+ return validated.data;
230
+ }
231
+ /**
232
+ * Reboot a server and wait for completion
233
+ *
234
+ * @param id - Server ID
235
+ * @param onProgress - Optional progress callback
236
+ */
237
+ async rebootAndWait(id, onProgress) {
238
+ const action = await this.reboot(id);
239
+ return await this.client.actions.waitFor(action.id, { onProgress });
240
+ }
241
+ /**
242
+ * Shutdown a server gracefully
243
+ *
244
+ * @param id - Server ID
245
+ * @returns Action for server shutdown
246
+ */
247
+ async shutdown(id) {
248
+ // Validate ID with Zod
249
+ const serverIdSchema = z.number().int().positive();
250
+ const validatedId = serverIdSchema.safeParse(id);
251
+ if (!validatedId.success) {
252
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
253
+ }
254
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/shutdown`, { method: "POST" });
255
+ const validated = HetznerActionSchema.safeParse(response.action);
256
+ if (!validated.success) {
257
+ console.warn('Hetzner shutdown validation warning:', validated.error.issues);
258
+ return response.action;
259
+ }
260
+ return validated.data;
261
+ }
262
+ /**
263
+ * Shutdown a server and wait for completion
264
+ *
265
+ * @param id - Server ID
266
+ * @param onProgress - Optional progress callback
267
+ */
268
+ async shutdownAndWait(id, onProgress) {
269
+ const action = await this.shutdown(id);
270
+ return await this.client.actions.waitFor(action.id, { onProgress });
271
+ }
272
+ /**
273
+ * Reset a server
274
+ *
275
+ * @param id - Server ID
276
+ * @returns Action for server reset
277
+ */
278
+ async reset(id) {
279
+ // Validate ID with Zod
280
+ const serverIdSchema = z.number().int().positive();
281
+ const validatedId = serverIdSchema.safeParse(id);
282
+ if (!validatedId.success) {
283
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
284
+ }
285
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/reset`, { method: "POST" });
286
+ const validated = HetznerActionSchema.safeParse(response.action);
287
+ if (!validated.success) {
288
+ console.warn('Hetzner reset validation warning:', validated.error.issues);
289
+ return response.action;
290
+ }
291
+ return validated.data;
292
+ }
293
+ /**
294
+ * Reset a server and wait for completion
295
+ *
296
+ * @param id - Server ID
297
+ * @param onProgress - Optional progress callback
298
+ */
299
+ async resetAndWait(id, onProgress) {
300
+ const action = await this.reset(id);
301
+ return await this.client.actions.waitFor(action.id, { onProgress });
302
+ }
303
+ /**
304
+ * Rebuild a server from an image
305
+ *
306
+ * @param id - Server ID
307
+ * @param image - Image ID or name
308
+ * @returns Action for server rebuild
309
+ */
310
+ async rebuild(id, image) {
311
+ // Validate ID with Zod
312
+ const serverIdSchema = z.number().int().positive();
313
+ const validatedId = serverIdSchema.safeParse(id);
314
+ if (!validatedId.success) {
315
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
316
+ }
317
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/rebuild`, {
318
+ method: "POST",
319
+ body: JSON.stringify({ image }),
320
+ });
321
+ const validated = HetznerActionSchema.safeParse(response.action);
322
+ if (!validated.success) {
323
+ console.warn('Hetzner rebuild validation warning:', validated.error.issues);
324
+ return response.action;
325
+ }
326
+ return validated.data;
327
+ }
328
+ /**
329
+ * Enable rescue mode for a server
330
+ *
331
+ * @param id - Server ID
332
+ * @param options - Rescue mode options
333
+ * @returns Action for enabling rescue mode
334
+ */
335
+ async enableRescue(id, options) {
336
+ // Validate ID with Zod
337
+ const serverIdSchema = z.number().int().positive();
338
+ const validatedId = serverIdSchema.safeParse(id);
339
+ if (!validatedId.success) {
340
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
341
+ }
342
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/enable_rescue`, {
343
+ method: "POST",
344
+ body: JSON.stringify(options || {}),
345
+ });
346
+ const validated = HetznerActionSchema.safeParse(response.action);
347
+ if (!validated.success) {
348
+ console.warn('Hetzner enable rescue validation warning:', validated.error.issues);
349
+ return response.action;
350
+ }
351
+ return validated.data;
352
+ }
353
+ /**
354
+ * Disable rescue mode for a server
355
+ *
356
+ * @param id - Server ID
357
+ * @returns Action for disabling rescue mode
358
+ */
359
+ async disableRescue(id) {
360
+ // Validate ID with Zod
361
+ const serverIdSchema = z.number().int().positive();
362
+ const validatedId = serverIdSchema.safeParse(id);
363
+ if (!validatedId.success) {
364
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
365
+ }
366
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/disable_rescue`, { method: "POST" });
367
+ const validated = HetznerActionSchema.safeParse(response.action);
368
+ if (!validated.success) {
369
+ console.warn('Hetzner disable rescue validation warning:', validated.error.issues);
370
+ return response.action;
371
+ }
372
+ return validated.data;
373
+ }
374
+ /**
375
+ * Change server type
376
+ *
377
+ * @param id - Server ID
378
+ * @param serverType - New server type
379
+ * @param upgradeDisk - Whether to upgrade disk (default: false)
380
+ * @returns Action for changing server type
381
+ */
382
+ async changeType(id, serverType, upgradeDisk = false) {
383
+ // Validate ID with Zod
384
+ const serverIdSchema = z.number().int().positive();
385
+ const validatedId = serverIdSchema.safeParse(id);
386
+ if (!validatedId.success) {
387
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
388
+ }
389
+ const response = await this.client.request(`/servers/${validatedId.data}/actions/change_type`, {
390
+ method: "POST",
391
+ body: JSON.stringify({ server_type: serverType, upgrade_disk: upgradeDisk }),
392
+ });
393
+ const validated = HetznerActionSchema.safeParse(response.action);
394
+ if (!validated.success) {
395
+ console.warn('Hetzner change type validation warning:', validated.error.issues);
396
+ return response.action;
397
+ }
398
+ return validated.data;
399
+ }
400
+ /**
401
+ * Get actions for a specific server
402
+ *
403
+ * @param id - Server ID
404
+ * @param options - Optional filters (status, sort, etc.)
405
+ * @returns Array of server actions
406
+ */
407
+ async getActions(id, options) {
408
+ // Validate ID with Zod
409
+ const serverIdSchema = z.number().int().positive();
410
+ const validatedId = serverIdSchema.safeParse(id);
411
+ if (!validatedId.success) {
412
+ throw new Error(`Invalid server ID: ${validatedId.error.issues.map(i => i.message).join(', ')}`);
413
+ }
414
+ const params = new URLSearchParams();
415
+ if (options?.status)
416
+ params.append("status", options.status);
417
+ if (options?.sort)
418
+ params.append("sort", options.sort);
419
+ const query = params.toString();
420
+ const response = await this.client.request(`/servers/${validatedId.data}/actions${query ? `?${query}` : ""}`);
421
+ return response.actions;
422
+ }
423
+ }
424
+ //# sourceMappingURL=servers.js.map