@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
package/actions.js ADDED
@@ -0,0 +1,802 @@
1
+ /**
2
+ * Hetzner Cloud API action polling and management
3
+ *
4
+ * Provides a robust polling service for monitoring async Hetzner actions
5
+ * with progress tracking, error handling, and cancellation support.
6
+ */
7
+ import { ActionCommand, } from "./types.js";
8
+ import { HetznerActionError, HetznerTimeoutError, isRetryableError, calculateRetryDelay, } from "./errors.js";
9
+ // ============================================================================
10
+ // Action Operations Class
11
+ // ============================================================================
12
+ /**
13
+ * Action operations for the Hetzner Cloud API
14
+ *
15
+ * Provides methods for managing and polling actions with enhanced
16
+ * polling capabilities including cancellation, progress tracking,
17
+ * and batch operations.
18
+ */
19
+ export class ActionOperations {
20
+ client;
21
+ constructor(client) {
22
+ this.client = client;
23
+ }
24
+ /**
25
+ * Get a specific action by ID
26
+ *
27
+ * @param id - Action ID
28
+ * @returns Action details
29
+ */
30
+ async get(id) {
31
+ const response = await this.client.request(`/actions/${id}`);
32
+ return response.action;
33
+ }
34
+ /**
35
+ * List all actions
36
+ *
37
+ * @param options - Optional filters (status, sort, etc.)
38
+ * @returns Array of actions
39
+ */
40
+ async list(options) {
41
+ const params = new URLSearchParams();
42
+ if (options?.status)
43
+ params.append("status", options.status);
44
+ if (options?.sort)
45
+ params.append("sort", options.sort);
46
+ if (options?.page)
47
+ params.append("page", options.page.toString());
48
+ if (options?.per_page)
49
+ params.append("per_page", options.per_page.toString());
50
+ const query = params.toString();
51
+ const response = await this.client.request(`/actions${query ? `?${query}` : ""}`);
52
+ return response.actions;
53
+ }
54
+ /**
55
+ * Poll a single action until completion (enhanced version)
56
+ *
57
+ * This is the main polling service method with full feature support:
58
+ * - Progress tracking with onProgress callback
59
+ * - Success/failure callbacks
60
+ * - Cancellation via AbortSignal
61
+ * - Adaptive polling intervals
62
+ * - Comprehensive error handling
63
+ *
64
+ * @param actionId - Action ID to poll
65
+ * @param options - Polling options
66
+ * @returns Promise resolving to the completed action
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const action = await client.actions.poll(123, {
71
+ * onProgress: (a) => console.log(`Progress: ${a.progress}%`),
72
+ * onComplete: (a) => console.log('Done!'),
73
+ * onError: (e) => console.error('Failed:', e.message),
74
+ * timeout: 300000, // 5 minutes
75
+ * signal: abortController.signal
76
+ * });
77
+ * ```
78
+ */
79
+ async poll(actionId, options = {}) {
80
+ return pollAction(this.client, actionId, options);
81
+ }
82
+ /**
83
+ * Poll multiple actions until completion
84
+ *
85
+ * Handles concurrent polling of multiple actions with optional
86
+ * concurrency limit and individual action tracking.
87
+ *
88
+ * @param actionIds - Array of action IDs to poll
89
+ * @param options - Polling options
90
+ * @returns Promise resolving to array of completed actions
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const actions = await client.actions.pollMany([1, 2, 3], {
95
+ * onProgress: (a) => updateUI(a),
96
+ * concurrency: 3, // Poll 3 at a time
97
+ * signal: abortController.signal
98
+ * });
99
+ * ```
100
+ */
101
+ async pollMany(actionIds, options = {}) {
102
+ return pollActions(this.client, actionIds, options);
103
+ }
104
+ /**
105
+ * Poll multiple actions with detailed result tracking
106
+ *
107
+ * Returns a BatchPollingResult with success/failure counts and
108
+ * per-action results for better error handling.
109
+ *
110
+ * @param actionIds - Array of action IDs to poll
111
+ * @param options - Polling options
112
+ * @returns Batch polling result with detailed status
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const result = await client.actions.pollManyDetailed([1, 2, 3], {
117
+ * concurrency: 2
118
+ * });
119
+ * console.log(`Success: ${result.successful}, Failed: ${result.failed}`);
120
+ * for (const [id, r] of result.results) {
121
+ * if (r.error) console.error(`Action ${id} failed:`, r.error);
122
+ * }
123
+ * ```
124
+ */
125
+ async pollManyDetailed(actionIds, options = {}) {
126
+ return pollActionsDetailed(this.client, actionIds, options);
127
+ }
128
+ /**
129
+ * Wait for an action to complete (backward compatible alias)
130
+ *
131
+ * @param id - Action ID
132
+ * @param options - Polling options
133
+ * @returns Completed action
134
+ * @deprecated Use poll() instead for new code
135
+ */
136
+ async waitFor(id, options) {
137
+ return waitForAction(this.client, id, options);
138
+ }
139
+ /**
140
+ * Wait for multiple actions to complete (backward compatible alias)
141
+ *
142
+ * @param ids - Array of action IDs
143
+ * @param options - Polling options
144
+ * @returns Array of completed actions
145
+ * @deprecated Use pollMany() instead for new code
146
+ */
147
+ async waitForMany(ids, options) {
148
+ return waitForMultipleActions(this.client, ids, options);
149
+ }
150
+ /**
151
+ * Batch check multiple actions (no polling, single fetch)
152
+ *
153
+ * @param ids - Array of action IDs
154
+ * @returns Map of action ID to action
155
+ */
156
+ async batchCheck(ids) {
157
+ return batchCheckActions(this.client, ids);
158
+ }
159
+ }
160
+ // ============================================================================
161
+ // Action Timeouts
162
+ // ============================================================================
163
+ /**
164
+ * Default timeouts for different action types (in milliseconds)
165
+ * Adjusted based on typical operation durations
166
+ */
167
+ export const ACTION_TIMEOUTS = {
168
+ // Quick operations (under 1 minute)
169
+ [ActionCommand.StartServer]: 60000,
170
+ [ActionCommand.StopServer]: 60000,
171
+ [ActionCommand.RebootServer]: 120000,
172
+ [ActionCommand.Poweroff]: 60000,
173
+ [ActionCommand.ShutdownServer]: 60000,
174
+ [ActionCommand.ResetServer]: 60000,
175
+ // Medium operations (1-5 minutes)
176
+ [ActionCommand.CreateServer]: 300000,
177
+ [ActionCommand.DeleteServer]: 180000,
178
+ [ActionCommand.ChangeServerType]: 600000,
179
+ [ActionCommand.ChangeDnsPtr]: 30000,
180
+ // Long operations (5-30 minutes)
181
+ [ActionCommand.RebuildServer]: 900000,
182
+ [ActionCommand.CreateImage]: 1800000,
183
+ [ActionCommand.EnableRescue]: 120000,
184
+ [ActionCommand.DisableRescue]: 60000,
185
+ // Volume operations
186
+ [ActionCommand.CreateVolume]: 300000,
187
+ [ActionCommand.DeleteVolume]: 180000,
188
+ [ActionCommand.AttachVolume]: 120000,
189
+ [ActionCommand.DetachVolume]: 60000,
190
+ [ActionCommand.ResizeVolume]: 600000,
191
+ // Network operations
192
+ [ActionCommand.AddSubnet]: 60000,
193
+ [ActionCommand.DeleteSubnet]: 60000,
194
+ [ActionCommand.AddRoute]: 60000,
195
+ [ActionCommand.DeleteRoute]: 60000,
196
+ [ActionCommand.ChangeIpRange]: 120000,
197
+ [ActionCommand.AttachToNetwork]: 60000,
198
+ [ActionCommand.DetachFromNetwork]: 60000,
199
+ // Floating IP operations
200
+ [ActionCommand.AssignFloatingIp]: 60000,
201
+ [ActionCommand.UnassignFloatingIp]: 60000,
202
+ // Load Balancer operations
203
+ [ActionCommand.CreateLoadBalancer]: 300000,
204
+ [ActionCommand.DeleteLoadBalancer]: 180000,
205
+ [ActionCommand.AddTarget]: 60000,
206
+ [ActionCommand.RemoveTarget]: 60000,
207
+ [ActionCommand.AddService]: 60000,
208
+ [ActionCommand.UpdateService]: 60000,
209
+ [ActionCommand.DeleteService]: 60000,
210
+ [ActionCommand.LoadBalancerAttachToNetwork]: 60000,
211
+ [ActionCommand.LoadBalancerDetachFromNetwork]: 60000,
212
+ [ActionCommand.ChangeAlgorithm]: 60000,
213
+ [ActionCommand.ChangeType]: 60000,
214
+ // Certificate operations
215
+ [ActionCommand.IssueCertificate]: 600000, // Can take up to 10 minutes
216
+ [ActionCommand.RetryCertificate]: 600000,
217
+ // Firewall operations
218
+ [ActionCommand.SetFirewallRules]: 60000,
219
+ [ActionCommand.ApplyFirewall]: 120000,
220
+ [ActionCommand.RemoveFirewall]: 60000,
221
+ // Floating IP DNS
222
+ [ActionCommand.FloatingIpChangeDnsPtr]: 30000,
223
+ // Backup operations
224
+ [ActionCommand.EnableBackup]: 60000,
225
+ [ActionCommand.DisableBackup]: 60000,
226
+ // Protection operations
227
+ [ActionCommand.ChangeProtection]: 30000,
228
+ [ActionCommand.VolumeChangeProtection]: 30000,
229
+ [ActionCommand.NetworkChangeProtection]: 30000,
230
+ [ActionCommand.FloatingIpChangeProtection]: 30000,
231
+ [ActionCommand.LoadBalancerChangeProtection]: 30000,
232
+ [ActionCommand.FirewallChangeProtection]: 30000,
233
+ [ActionCommand.ImageChangeProtection]: 30000,
234
+ // Other operations
235
+ [ActionCommand.ChangeAliasIps]: 60000,
236
+ };
237
+ /**
238
+ * Get default timeout for an action command
239
+ */
240
+ export function getActionTimeout(command) {
241
+ return ACTION_TIMEOUTS[command] || 300000; // Default 5 minutes
242
+ }
243
+ // ============================================================================
244
+ // Enhanced Action Polling Service
245
+ // ============================================================================
246
+ /**
247
+ * Default polling options
248
+ */
249
+ const DEFAULT_POLLING_OPTIONS = {
250
+ pollInterval: 2000,
251
+ maxRetries: 60,
252
+ timeout: 300000,
253
+ onProgress: () => { },
254
+ onComplete: () => { },
255
+ onError: () => { },
256
+ onRetry: () => { },
257
+ signal: undefined,
258
+ adaptive: false,
259
+ concurrency: 5,
260
+ };
261
+ /**
262
+ * Poll for an action to complete with full feature support
263
+ *
264
+ * This is the core polling service function that provides:
265
+ * - Progress tracking via onProgress callback
266
+ * - Success/error notification via onComplete/onError
267
+ * - Cancellation support via AbortSignal
268
+ * - Adaptive polling intervals
269
+ * - Retry logic with exponential backoff
270
+ *
271
+ * @param client - Hetzner API client
272
+ * @param actionId - Action ID to poll
273
+ * @param options - Polling options
274
+ * @returns Completed action
275
+ * @throws {HetznerActionError} If action fails
276
+ * @throws {HetznerTimeoutError} If action times out
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * const action = await pollAction(client, 123, {
281
+ * onProgress: (a) => console.log(`Progress: ${a.progress}%`),
282
+ * onComplete: (a) => console.log('Completed:', a.command),
283
+ * onError: (e) => console.error('Failed:', e.message),
284
+ * timeout: 60000,
285
+ * signal: abortSignal
286
+ * });
287
+ * ```
288
+ */
289
+ export async function pollAction(client, actionId, options = {}) {
290
+ const opts = { ...DEFAULT_POLLING_OPTIONS, ...options };
291
+ const startTime = Date.now();
292
+ let lastProgress = 0;
293
+ let lastError = null;
294
+ let attempt = 0;
295
+ // Check if already aborted
296
+ if (opts.signal?.aborted) {
297
+ throw new Error("Polling aborted before start");
298
+ }
299
+ // Set up abort listener
300
+ const abortListener = () => {
301
+ throw new Error("Polling aborted");
302
+ };
303
+ opts.signal?.addEventListener("abort", abortListener);
304
+ try {
305
+ while (attempt < opts.maxRetries) {
306
+ // Check timeout
307
+ const elapsed = Date.now() - startTime;
308
+ if (elapsed > opts.timeout) {
309
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
310
+ }
311
+ try {
312
+ const response = await client.request(`/actions/${actionId}`);
313
+ const action = response.action;
314
+ attempt++;
315
+ // Notify progress callback on any change
316
+ if (action.progress !== lastProgress) {
317
+ lastProgress = action.progress;
318
+ opts.onProgress(action);
319
+ }
320
+ // Check if action completed successfully
321
+ if (action.status === "success") {
322
+ opts.onComplete(action);
323
+ return action;
324
+ }
325
+ // Check if action failed
326
+ if (action.status === "error") {
327
+ const error = new HetznerActionError(action.error, actionId);
328
+ opts.onError(error, action);
329
+ throw error;
330
+ }
331
+ // Still running - calculate wait time
332
+ let waitTime = opts.pollInterval;
333
+ if (opts.adaptive) {
334
+ waitTime = getAdaptivePollInterval(action.progress);
335
+ }
336
+ await sleep(waitTime);
337
+ }
338
+ catch (error) {
339
+ // Check if this is a retryable error
340
+ if (isRetryableError(error)) {
341
+ const delay = calculateRetryDelay(attempt);
342
+ opts.onRetry(attempt, delay);
343
+ if (opts.signal?.aborted) {
344
+ throw new Error("Polling aborted during retry");
345
+ }
346
+ await sleep(delay);
347
+ continue;
348
+ }
349
+ // Non-retryable error - throw immediately
350
+ throw error;
351
+ }
352
+ }
353
+ // Max retries exceeded
354
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
355
+ }
356
+ finally {
357
+ opts.signal?.removeEventListener("abort", abortListener);
358
+ }
359
+ }
360
+ /**
361
+ * Poll multiple actions concurrently
362
+ *
363
+ * Manages polling of multiple actions with optional concurrency limit.
364
+ * Each action is polled independently with shared callbacks.
365
+ *
366
+ * @param client - Hetzner API client
367
+ * @param actionIds - Array of action IDs to poll
368
+ * @param options - Polling options
369
+ * @returns Array of completed actions in same order as input
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * const actions = await pollActions(client, [1, 2, 3], {
374
+ * onProgress: (a) => console.log(`Action ${a.id}: ${a.progress}%`),
375
+ * onComplete: (a) => console.log(`Action ${a.id} complete`),
376
+ * concurrency: 3 // Poll 3 actions at a time
377
+ * });
378
+ * ```
379
+ */
380
+ export async function pollActions(client, actionIds, options = {}) {
381
+ const concurrency = options.concurrency ?? DEFAULT_POLLING_OPTIONS.concurrency;
382
+ // If concurrency is high enough, poll all at once
383
+ if (concurrency >= actionIds.length) {
384
+ const promises = actionIds.map((id) => pollAction(client, id, options));
385
+ return Promise.all(promises);
386
+ }
387
+ // Otherwise, process in batches with concurrency limit
388
+ const results = new Array(actionIds.length);
389
+ let currentIndex = 0;
390
+ const processBatch = async () => {
391
+ while (currentIndex < actionIds.length) {
392
+ const batchStart = currentIndex;
393
+ const batchEnd = Math.min(currentIndex + concurrency, actionIds.length);
394
+ const batchIds = actionIds.slice(batchStart, batchEnd);
395
+ currentIndex = batchEnd;
396
+ const batchResults = await Promise.all(batchIds.map((id, i) => pollAction(client, id, options)));
397
+ // Store results in correct positions
398
+ batchResults.forEach((result, i) => {
399
+ results[batchStart + i] = result;
400
+ });
401
+ }
402
+ };
403
+ // Process batches sequentially but actions within each batch concurrently
404
+ await processBatch();
405
+ return results;
406
+ }
407
+ /**
408
+ * Poll multiple actions with detailed result tracking
409
+ *
410
+ * Similar to pollActions but returns a BatchPollingResult with
411
+ * success/failure counts and per-action results for better
412
+ * error handling and monitoring.
413
+ *
414
+ * @param client - Hetzner API client
415
+ * @param actionIds - Array of action IDs to poll
416
+ * @param options - Polling options
417
+ * @returns Batch polling result with detailed status
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * const result = await pollActionsDetailed(client, [1, 2, 3], {
422
+ * onProgress: (a) => updateProgressBar(a)
423
+ * });
424
+ *
425
+ * console.log(`Completed: ${result.successful}/${actionIds.length}`);
426
+ * for (const [id, r] of result.results) {
427
+ * if (r.error) {
428
+ * console.error(`Action ${id} failed:`, r.error.message);
429
+ * }
430
+ * }
431
+ * ```
432
+ */
433
+ export async function pollActionsDetailed(client, actionIds, options = {}) {
434
+ const startTime = Date.now();
435
+ const results = new Map();
436
+ let successful = 0;
437
+ let failed = 0;
438
+ // Wrap callbacks to track results
439
+ const wrappedOptions = {
440
+ ...options,
441
+ onComplete: (action) => {
442
+ results.set(action.id, {
443
+ success: true,
444
+ action,
445
+ attempts: 0,
446
+ elapsed: Date.now() - startTime,
447
+ });
448
+ successful++;
449
+ options.onComplete?.(action);
450
+ },
451
+ onError: (error, action) => {
452
+ results.set(action.id, {
453
+ success: false,
454
+ action,
455
+ error,
456
+ attempts: 0,
457
+ elapsed: Date.now() - startTime,
458
+ });
459
+ failed++;
460
+ options.onError?.(error, action);
461
+ },
462
+ };
463
+ try {
464
+ // Poll all actions
465
+ await pollActions(client, actionIds, wrappedOptions);
466
+ }
467
+ catch (error) {
468
+ // Some actions may have failed, but we still return results
469
+ console.warn("Some actions failed during batch polling:", error);
470
+ }
471
+ return {
472
+ results,
473
+ successful,
474
+ failed,
475
+ elapsed: Date.now() - startTime,
476
+ };
477
+ }
478
+ // ============================================================================
479
+ // Backward Compatible Polling Functions
480
+ // ============================================================================
481
+ /**
482
+ * Poll for an action to complete (backward compatible)
483
+ *
484
+ * @deprecated Use pollAction() instead for new code
485
+ */
486
+ export async function waitForAction(client, actionId, options = {}) {
487
+ const opts = { ...DEFAULT_POLLING_OPTIONS, ...options };
488
+ const startTime = Date.now();
489
+ let lastProgress = 0;
490
+ let lastError = null;
491
+ for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
492
+ // Check timeout
493
+ if (Date.now() - startTime > opts.timeout) {
494
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
495
+ }
496
+ try {
497
+ const action = await client.request(`/actions/${actionId}`);
498
+ // Notify progress callback
499
+ if (action.action.progress !== lastProgress) {
500
+ lastProgress = action.action.progress;
501
+ opts.onProgress(action.action);
502
+ }
503
+ // Check if action is complete
504
+ if (action.action.status === "success") {
505
+ return action.action;
506
+ }
507
+ if (action.action.status === "error") {
508
+ throw new HetznerActionError(action.action.error, actionId);
509
+ }
510
+ // Still running - wait before next poll
511
+ await sleep(opts.pollInterval);
512
+ }
513
+ catch (error) {
514
+ // Check if error is retryable
515
+ if (isRetryableError(error)) {
516
+ const delay = calculateRetryDelay(attempt);
517
+ console.warn(`Retrying action ${actionId} after ${delay}ms (attempt ${attempt + 1}/${opts.maxRetries})`);
518
+ await sleep(delay);
519
+ continue;
520
+ }
521
+ // Non-retryable error - throw immediately
522
+ throw error;
523
+ }
524
+ }
525
+ // Max retries exceeded
526
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
527
+ }
528
+ /**
529
+ * Poll for multiple actions concurrently (backward compatible)
530
+ *
531
+ * @deprecated Use pollActions() instead for new code
532
+ */
533
+ export async function waitForMultipleActions(client, actionIds, options = {}) {
534
+ const promises = actionIds.map((id) => waitForAction(client, id, options));
535
+ return Promise.all(promises);
536
+ }
537
+ /**
538
+ * Poll for multiple actions with concurrency limit (backward compatible)
539
+ *
540
+ * @deprecated Use pollActions() with concurrency option instead
541
+ */
542
+ export async function waitForMultipleActionsWithLimit(client, actionIds, concurrency = 5, options = {}) {
543
+ const results = [];
544
+ const chunks = [];
545
+ // Split action IDs into chunks
546
+ for (let i = 0; i < actionIds.length; i += concurrency) {
547
+ chunks.push(actionIds.slice(i, i + concurrency));
548
+ }
549
+ // Process each chunk sequentially
550
+ for (const chunk of chunks) {
551
+ const chunkResults = await waitForMultipleActions(client, chunk, options);
552
+ results.push(...chunkResults);
553
+ }
554
+ return results;
555
+ }
556
+ /**
557
+ * Batch check multiple actions in a single API call
558
+ *
559
+ * Uses the /actions?id=42&id=43 endpoint to fetch multiple actions at once
560
+ *
561
+ * @param client - Hetzner API client
562
+ * @param actionIds - Array of action IDs to check
563
+ * @returns Map of action ID to action
564
+ */
565
+ export async function batchCheckActions(client, actionIds) {
566
+ const MAX_BATCH_SIZE = 50; // API limit for ID parameter
567
+ const results = new Map();
568
+ // Process in batches
569
+ for (let i = 0; i < actionIds.length; i += MAX_BATCH_SIZE) {
570
+ const batch = actionIds.slice(i, i + MAX_BATCH_SIZE);
571
+ const idParams = batch.map((id) => `id=${id}`).join("&");
572
+ const response = await client.request(`/actions?${idParams}`);
573
+ for (const action of response.actions) {
574
+ results.set(action.id, action);
575
+ }
576
+ }
577
+ return results;
578
+ }
579
+ // ============================================================================
580
+ // Action Status Utilities
581
+ // ============================================================================
582
+ /**
583
+ * Check if an action is running
584
+ */
585
+ export function isActionRunning(action) {
586
+ return action.status === "running";
587
+ }
588
+ /**
589
+ * Check if an action completed successfully
590
+ */
591
+ export function isActionSuccess(action) {
592
+ return action.status === "success";
593
+ }
594
+ /**
595
+ * Check if an action failed
596
+ */
597
+ export function isActionError(action) {
598
+ return action.status === "error";
599
+ }
600
+ /**
601
+ * Format action progress for display
602
+ */
603
+ export function formatActionProgress(action) {
604
+ const command = action.command.replace(/_/g, " ");
605
+ const status = action.status.toUpperCase();
606
+ return `${status}: ${command} (${action.progress}%)`;
607
+ }
608
+ /**
609
+ * Get human-readable action description
610
+ */
611
+ export function getActionDescription(command) {
612
+ const descriptions = {
613
+ [ActionCommand.CreateServer]: "Creating server",
614
+ [ActionCommand.DeleteServer]: "Deleting server",
615
+ [ActionCommand.StartServer]: "Starting server",
616
+ [ActionCommand.StopServer]: "Stopping server",
617
+ [ActionCommand.RebootServer]: "Rebooting server",
618
+ [ActionCommand.ResetServer]: "Resetting server",
619
+ [ActionCommand.ShutdownServer]: "Shutting down server",
620
+ [ActionCommand.Poweroff]: "Cutting power to server",
621
+ [ActionCommand.ChangeServerType]: "Changing server type",
622
+ [ActionCommand.RebuildServer]: "Rebuilding server",
623
+ [ActionCommand.EnableBackup]: "Enabling backups",
624
+ [ActionCommand.DisableBackup]: "Disabling backups",
625
+ [ActionCommand.CreateImage]: "Creating image",
626
+ [ActionCommand.ChangeDnsPtr]: "Changing reverse DNS",
627
+ [ActionCommand.AttachToNetwork]: "Attaching to network",
628
+ [ActionCommand.DetachFromNetwork]: "Detaching from network",
629
+ [ActionCommand.ChangeAliasIps]: "Changing alias IPs",
630
+ [ActionCommand.EnableRescue]: "Enabling rescue mode",
631
+ [ActionCommand.DisableRescue]: "Disabling rescue mode",
632
+ [ActionCommand.ChangeProtection]: "Changing protection",
633
+ [ActionCommand.CreateVolume]: "Creating volume",
634
+ [ActionCommand.DeleteVolume]: "Deleting volume",
635
+ [ActionCommand.AttachVolume]: "Attaching volume",
636
+ [ActionCommand.DetachVolume]: "Detaching volume",
637
+ [ActionCommand.ResizeVolume]: "Resizing volume",
638
+ [ActionCommand.VolumeChangeProtection]: "Changing volume protection",
639
+ [ActionCommand.AddSubnet]: "Adding subnet",
640
+ [ActionCommand.DeleteSubnet]: "Deleting subnet",
641
+ [ActionCommand.AddRoute]: "Adding route",
642
+ [ActionCommand.DeleteRoute]: "Deleting route",
643
+ [ActionCommand.ChangeIpRange]: "Changing IP range",
644
+ [ActionCommand.NetworkChangeProtection]: "Changing network protection",
645
+ [ActionCommand.AssignFloatingIp]: "Assigning floating IP",
646
+ [ActionCommand.UnassignFloatingIp]: "Unassigning floating IP",
647
+ [ActionCommand.FloatingIpChangeDnsPtr]: "Changing floating IP DNS",
648
+ [ActionCommand.FloatingIpChangeProtection]: "Changing floating IP protection",
649
+ [ActionCommand.CreateLoadBalancer]: "Creating load balancer",
650
+ [ActionCommand.DeleteLoadBalancer]: "Deleting load balancer",
651
+ [ActionCommand.AddTarget]: "Adding target to load balancer",
652
+ [ActionCommand.RemoveTarget]: "Removing target from load balancer",
653
+ [ActionCommand.AddService]: "Adding service to load balancer",
654
+ [ActionCommand.UpdateService]: "Updating load balancer service",
655
+ [ActionCommand.DeleteService]: "Deleting load balancer service",
656
+ [ActionCommand.LoadBalancerAttachToNetwork]: "Attaching load balancer to network",
657
+ [ActionCommand.LoadBalancerDetachFromNetwork]: "Detaching load balancer from network",
658
+ [ActionCommand.ChangeAlgorithm]: "Changing load balancer algorithm",
659
+ [ActionCommand.ChangeType]: "Changing load balancer type",
660
+ [ActionCommand.LoadBalancerChangeProtection]: "Changing load balancer protection",
661
+ [ActionCommand.IssueCertificate]: "Issuing certificate",
662
+ [ActionCommand.RetryCertificate]: "Retrying certificate",
663
+ [ActionCommand.SetFirewallRules]: "Setting firewall rules",
664
+ [ActionCommand.ApplyFirewall]: "Applying firewall",
665
+ [ActionCommand.RemoveFirewall]: "Removing firewall",
666
+ [ActionCommand.FirewallChangeProtection]: "Changing firewall protection",
667
+ [ActionCommand.ImageChangeProtection]: "Changing image protection",
668
+ };
669
+ return descriptions[command] || command.replace(/_/g, " ");
670
+ }
671
+ // ============================================================================
672
+ // Adaptive Polling
673
+ // ============================================================================
674
+ /**
675
+ * Get adaptive polling interval based on operation type
676
+ */
677
+ export function getPollInterval(command) {
678
+ const intervals = {
679
+ // Quick operations - poll frequently
680
+ [ActionCommand.StartServer]: 5000,
681
+ [ActionCommand.StopServer]: 5000,
682
+ [ActionCommand.RebootServer]: 10000,
683
+ [ActionCommand.Poweroff]: 5000,
684
+ [ActionCommand.ShutdownServer]: 5000,
685
+ // Medium operations
686
+ [ActionCommand.CreateServer]: 15000,
687
+ [ActionCommand.CreateVolume]: 10000,
688
+ [ActionCommand.AttachVolume]: 8000,
689
+ [ActionCommand.DetachVolume]: 5000,
690
+ // Long operations - poll less frequently
691
+ [ActionCommand.RebuildServer]: 30000,
692
+ [ActionCommand.CreateImage]: 60000,
693
+ [ActionCommand.IssueCertificate]: 30000,
694
+ };
695
+ return intervals[command] || 10000; // Default 10 seconds
696
+ }
697
+ /**
698
+ * Get adaptive polling interval based on action progress
699
+ */
700
+ export function getAdaptivePollInterval(progress) {
701
+ if (progress < 10)
702
+ return 2000; // Quick progress at start
703
+ if (progress < 50)
704
+ return 5000; // Medium progress
705
+ if (progress < 90)
706
+ return 10000; // Slowing down
707
+ return 15000; // Near completion
708
+ }
709
+ /**
710
+ * Poll with adaptive intervals (backward compatible)
711
+ *
712
+ * @deprecated Use pollAction() with adaptive: true option instead
713
+ */
714
+ export async function waitForActionAdaptive(client, actionId, command, options = {}) {
715
+ const startTime = Date.now();
716
+ const timeout = options.timeout ?? getActionTimeout(command);
717
+ let lastProgress = -1;
718
+ while (Date.now() - startTime < timeout) {
719
+ const action = await client.request(`/actions/${actionId}`);
720
+ // Notify progress callback
721
+ if (action.action.progress !== lastProgress) {
722
+ lastProgress = action.action.progress;
723
+ options.onProgress?.(action.action);
724
+ }
725
+ if (action.action.status === "success") {
726
+ return action.action;
727
+ }
728
+ if (action.action.status === "error") {
729
+ throw new HetznerActionError(action.action.error, actionId);
730
+ }
731
+ // Adaptive polling based on progress
732
+ const interval = getAdaptivePollInterval(action.action.progress);
733
+ await sleep(interval);
734
+ }
735
+ throw new HetznerTimeoutError(actionId, timeout, lastProgress);
736
+ }
737
+ // ============================================================================
738
+ // Rate Limit Handling
739
+ // ============================================================================
740
+ /**
741
+ * Parse rate limit headers from response
742
+ */
743
+ export function parseRateLimitHeaders(headers) {
744
+ const limit = headers.get("RateLimit-Limit");
745
+ const remaining = headers.get("RateLimit-Remaining");
746
+ const reset = headers.get("RateLimit-Reset");
747
+ if (!limit || !remaining || !reset) {
748
+ return null;
749
+ }
750
+ return {
751
+ limit: parseInt(limit, 10),
752
+ remaining: parseInt(remaining, 10),
753
+ reset: parseInt(reset, 10),
754
+ };
755
+ }
756
+ /**
757
+ * Check if rate limit is low (should warn user)
758
+ */
759
+ export function isRateLimitLow(info, threshold = 100) {
760
+ return info.remaining < threshold;
761
+ }
762
+ /**
763
+ * Get human-readable rate limit status
764
+ */
765
+ export function formatRateLimitStatus(info) {
766
+ const resetDate = new Date(info.reset * 1000);
767
+ const remaining = info.remaining;
768
+ const limit = info.limit;
769
+ const percentage = ((remaining / limit) * 100).toFixed(1);
770
+ return `${remaining}/${limit} (${percentage}%) - resets at ${resetDate.toISOString()}`;
771
+ }
772
+ /**
773
+ * Wait for rate limit to reset
774
+ */
775
+ export async function waitForRateLimitReset(info) {
776
+ const resetTime = info.reset * 1000;
777
+ const now = Date.now();
778
+ const waitTime = Math.max(0, resetTime - now);
779
+ if (waitTime > 0) {
780
+ const waitSeconds = Math.ceil(waitTime / 1000);
781
+ console.log(`Rate limit exhausted. Waiting ${waitSeconds}s for reset...`);
782
+ await sleep(waitTime);
783
+ }
784
+ }
785
+ // ============================================================================
786
+ // Utilities
787
+ // ============================================================================
788
+ /**
789
+ * Sleep for specified milliseconds
790
+ */
791
+ function sleep(ms) {
792
+ return new Promise((resolve) => setTimeout(resolve, ms));
793
+ }
794
+ /**
795
+ * Create a progress logger for actions
796
+ */
797
+ export function createProgressLogger(prefix = "Action") {
798
+ return (action) => {
799
+ console.log(`[${prefix}] ${getActionDescription(action.command)}: ${action.progress}%`);
800
+ };
801
+ }
802
+ //# sourceMappingURL=actions.js.map