@ebowwa/hetzner-mcp 1.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 (227) hide show
  1. package/bun.lock +250 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +1928 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/hetzner/actions.d.ts +356 -0
  7. package/dist/lib/hetzner/actions.d.ts.map +1 -0
  8. package/dist/lib/hetzner/actions.js +804 -0
  9. package/dist/lib/hetzner/actions.js.map +1 -0
  10. package/dist/lib/hetzner/auth.d.ts +7 -0
  11. package/dist/lib/hetzner/auth.d.ts.map +1 -0
  12. package/dist/lib/hetzner/auth.js +35 -0
  13. package/dist/lib/hetzner/auth.js.map +1 -0
  14. package/dist/lib/hetzner/bootstrap/cloud-init.d.ts +79 -0
  15. package/dist/lib/hetzner/bootstrap/cloud-init.d.ts.map +1 -0
  16. package/dist/lib/hetzner/bootstrap/cloud-init.js +279 -0
  17. package/dist/lib/hetzner/bootstrap/cloud-init.js.map +1 -0
  18. package/dist/lib/hetzner/bootstrap/firewall.d.ts +119 -0
  19. package/dist/lib/hetzner/bootstrap/firewall.d.ts.map +1 -0
  20. package/dist/lib/hetzner/bootstrap/firewall.js +279 -0
  21. package/dist/lib/hetzner/bootstrap/firewall.js.map +1 -0
  22. package/dist/lib/hetzner/bootstrap/genesis.d.ts +83 -0
  23. package/dist/lib/hetzner/bootstrap/genesis.d.ts.map +1 -0
  24. package/dist/lib/hetzner/bootstrap/genesis.js +406 -0
  25. package/dist/lib/hetzner/bootstrap/genesis.js.map +1 -0
  26. package/dist/lib/hetzner/bootstrap/index.d.ts +30 -0
  27. package/dist/lib/hetzner/bootstrap/index.d.ts.map +1 -0
  28. package/dist/lib/hetzner/bootstrap/index.js +35 -0
  29. package/dist/lib/hetzner/bootstrap/index.js.map +1 -0
  30. package/dist/lib/hetzner/bootstrap/kernel-hardening.d.ts +70 -0
  31. package/dist/lib/hetzner/bootstrap/kernel-hardening.d.ts.map +1 -0
  32. package/dist/lib/hetzner/bootstrap/kernel-hardening.js +266 -0
  33. package/dist/lib/hetzner/bootstrap/kernel-hardening.js.map +1 -0
  34. package/dist/lib/hetzner/bootstrap/kernel-hardening.test.d.ts +7 -0
  35. package/dist/lib/hetzner/bootstrap/kernel-hardening.test.d.ts.map +1 -0
  36. package/dist/lib/hetzner/bootstrap/kernel-hardening.test.js +181 -0
  37. package/dist/lib/hetzner/bootstrap/kernel-hardening.test.js.map +1 -0
  38. package/dist/lib/hetzner/bootstrap/security-audit.d.ts +46 -0
  39. package/dist/lib/hetzner/bootstrap/security-audit.d.ts.map +1 -0
  40. package/dist/lib/hetzner/bootstrap/security-audit.js +118 -0
  41. package/dist/lib/hetzner/bootstrap/security-audit.js.map +1 -0
  42. package/dist/lib/hetzner/bootstrap/ssh-hardening.d.ts +68 -0
  43. package/dist/lib/hetzner/bootstrap/ssh-hardening.d.ts.map +1 -0
  44. package/dist/lib/hetzner/bootstrap/ssh-hardening.js +182 -0
  45. package/dist/lib/hetzner/bootstrap/ssh-hardening.js.map +1 -0
  46. package/dist/lib/hetzner/client.d.ts +63 -0
  47. package/dist/lib/hetzner/client.d.ts.map +1 -0
  48. package/dist/lib/hetzner/client.js +137 -0
  49. package/dist/lib/hetzner/client.js.map +1 -0
  50. package/dist/lib/hetzner/config.d.ts +5 -0
  51. package/dist/lib/hetzner/config.d.ts.map +1 -0
  52. package/dist/lib/hetzner/config.js +5 -0
  53. package/dist/lib/hetzner/config.js.map +1 -0
  54. package/dist/lib/hetzner/errors.d.ts +171 -0
  55. package/dist/lib/hetzner/errors.d.ts.map +1 -0
  56. package/dist/lib/hetzner/errors.js +270 -0
  57. package/dist/lib/hetzner/errors.js.map +1 -0
  58. package/dist/lib/hetzner/index.d.ts +21 -0
  59. package/dist/lib/hetzner/index.d.ts.map +1 -0
  60. package/dist/lib/hetzner/index.js +29 -0
  61. package/dist/lib/hetzner/index.js.map +1 -0
  62. package/dist/lib/hetzner/pricing.d.ts +207 -0
  63. package/dist/lib/hetzner/pricing.d.ts.map +1 -0
  64. package/dist/lib/hetzner/pricing.js +284 -0
  65. package/dist/lib/hetzner/pricing.js.map +1 -0
  66. package/dist/lib/hetzner/schemas.d.ts +1644 -0
  67. package/dist/lib/hetzner/schemas.d.ts.map +1 -0
  68. package/dist/lib/hetzner/schemas.js +660 -0
  69. package/dist/lib/hetzner/schemas.js.map +1 -0
  70. package/dist/lib/hetzner/server-status.d.ts +26 -0
  71. package/dist/lib/hetzner/server-status.d.ts.map +1 -0
  72. package/dist/lib/hetzner/server-status.js +64 -0
  73. package/dist/lib/hetzner/server-status.js.map +1 -0
  74. package/dist/lib/hetzner/servers.d.ts +165 -0
  75. package/dist/lib/hetzner/servers.d.ts.map +1 -0
  76. package/dist/lib/hetzner/servers.js +424 -0
  77. package/dist/lib/hetzner/servers.js.map +1 -0
  78. package/dist/lib/hetzner/ssh-keys.d.ts +36 -0
  79. package/dist/lib/hetzner/ssh-keys.d.ts.map +1 -0
  80. package/dist/lib/hetzner/ssh-keys.js +90 -0
  81. package/dist/lib/hetzner/ssh-keys.js.map +1 -0
  82. package/dist/lib/hetzner/ssh-setup.d.ts +48 -0
  83. package/dist/lib/hetzner/ssh-setup.d.ts.map +1 -0
  84. package/dist/lib/hetzner/ssh-setup.js +167 -0
  85. package/dist/lib/hetzner/ssh-setup.js.map +1 -0
  86. package/dist/lib/hetzner/types.d.ts +330 -0
  87. package/dist/lib/hetzner/types.d.ts.map +1 -0
  88. package/dist/lib/hetzner/types.js +96 -0
  89. package/dist/lib/hetzner/types.js.map +1 -0
  90. package/dist/lib/hetzner/volumes.d.ts +106 -0
  91. package/dist/lib/hetzner/volumes.d.ts.map +1 -0
  92. package/dist/lib/hetzner/volumes.js +172 -0
  93. package/dist/lib/hetzner/volumes.js.map +1 -0
  94. package/dist/lib/resources.d.ts +70 -0
  95. package/dist/lib/resources.d.ts.map +1 -0
  96. package/dist/lib/resources.js +115 -0
  97. package/dist/lib/resources.js.map +1 -0
  98. package/dist/lib/ssh/flags.d.ts +349 -0
  99. package/dist/lib/ssh/flags.d.ts.map +1 -0
  100. package/dist/lib/ssh/flags.js +322 -0
  101. package/dist/lib/ssh/flags.js.map +1 -0
  102. package/dist/lib/ssh/index.d.ts +5 -0
  103. package/dist/lib/ssh/index.d.ts.map +1 -0
  104. package/dist/lib/ssh/index.js +5 -0
  105. package/dist/lib/ssh/index.js.map +1 -0
  106. package/dist/lib/terminal/api.d.ts +8 -0
  107. package/dist/lib/terminal/api.d.ts.map +1 -0
  108. package/dist/lib/terminal/api.js +594 -0
  109. package/dist/lib/terminal/api.js.map +1 -0
  110. package/dist/lib/terminal/client.d.ts +15 -0
  111. package/dist/lib/terminal/client.d.ts.map +1 -0
  112. package/dist/lib/terminal/client.js +45 -0
  113. package/dist/lib/terminal/client.js.map +1 -0
  114. package/dist/lib/terminal/config.d.ts +86 -0
  115. package/dist/lib/terminal/config.d.ts.map +1 -0
  116. package/dist/lib/terminal/config.js +375 -0
  117. package/dist/lib/terminal/config.js.map +1 -0
  118. package/dist/lib/terminal/error.d.ts +8 -0
  119. package/dist/lib/terminal/error.d.ts.map +1 -0
  120. package/dist/lib/terminal/error.js +12 -0
  121. package/dist/lib/terminal/error.js.map +1 -0
  122. package/dist/lib/terminal/exec.d.ts +47 -0
  123. package/dist/lib/terminal/exec.d.ts.map +1 -0
  124. package/dist/lib/terminal/exec.js +107 -0
  125. package/dist/lib/terminal/exec.js.map +1 -0
  126. package/dist/lib/terminal/files.d.ts +124 -0
  127. package/dist/lib/terminal/files.d.ts.map +1 -0
  128. package/dist/lib/terminal/files.js +436 -0
  129. package/dist/lib/terminal/files.js.map +1 -0
  130. package/dist/lib/terminal/fingerprint.d.ts +67 -0
  131. package/dist/lib/terminal/fingerprint.d.ts.map +1 -0
  132. package/dist/lib/terminal/fingerprint.js +214 -0
  133. package/dist/lib/terminal/fingerprint.js.map +1 -0
  134. package/dist/lib/terminal/index.d.ts +14 -0
  135. package/dist/lib/terminal/index.d.ts.map +1 -0
  136. package/dist/lib/terminal/index.js +21 -0
  137. package/dist/lib/terminal/index.js.map +1 -0
  138. package/dist/lib/terminal/manager.d.ts +103 -0
  139. package/dist/lib/terminal/manager.d.ts.map +1 -0
  140. package/dist/lib/terminal/manager.js +237 -0
  141. package/dist/lib/terminal/manager.js.map +1 -0
  142. package/dist/lib/terminal/network-error-detector.d.ts +19 -0
  143. package/dist/lib/terminal/network-error-detector.d.ts.map +1 -0
  144. package/dist/lib/terminal/network-error-detector.js +98 -0
  145. package/dist/lib/terminal/network-error-detector.js.map +1 -0
  146. package/dist/lib/terminal/pool.d.ts +143 -0
  147. package/dist/lib/terminal/pool.d.ts.map +1 -0
  148. package/dist/lib/terminal/pool.js +554 -0
  149. package/dist/lib/terminal/pool.js.map +1 -0
  150. package/dist/lib/terminal/pty.d.ts +59 -0
  151. package/dist/lib/terminal/pty.d.ts.map +1 -0
  152. package/dist/lib/terminal/pty.js +224 -0
  153. package/dist/lib/terminal/pty.js.map +1 -0
  154. package/dist/lib/terminal/resources.d.ts +63 -0
  155. package/dist/lib/terminal/resources.d.ts.map +1 -0
  156. package/dist/lib/terminal/resources.js +62 -0
  157. package/dist/lib/terminal/resources.js.map +1 -0
  158. package/dist/lib/terminal/scp.d.ts +30 -0
  159. package/dist/lib/terminal/scp.d.ts.map +1 -0
  160. package/dist/lib/terminal/scp.js +74 -0
  161. package/dist/lib/terminal/scp.js.map +1 -0
  162. package/dist/lib/terminal/sessions.d.ts +101 -0
  163. package/dist/lib/terminal/sessions.d.ts.map +1 -0
  164. package/dist/lib/terminal/sessions.js +718 -0
  165. package/dist/lib/terminal/sessions.js.map +1 -0
  166. package/dist/lib/terminal/tmux-exec.d.ts +50 -0
  167. package/dist/lib/terminal/tmux-exec.d.ts.map +1 -0
  168. package/dist/lib/terminal/tmux-exec.js +79 -0
  169. package/dist/lib/terminal/tmux-exec.js.map +1 -0
  170. package/dist/lib/terminal/tmux-local.d.ts +273 -0
  171. package/dist/lib/terminal/tmux-local.d.ts.map +1 -0
  172. package/dist/lib/terminal/tmux-local.js +629 -0
  173. package/dist/lib/terminal/tmux-local.js.map +1 -0
  174. package/dist/lib/terminal/tmux-manager.d.ts +328 -0
  175. package/dist/lib/terminal/tmux-manager.d.ts.map +1 -0
  176. package/dist/lib/terminal/tmux-manager.js +667 -0
  177. package/dist/lib/terminal/tmux-manager.js.map +1 -0
  178. package/dist/lib/terminal/tmux.d.ts +213 -0
  179. package/dist/lib/terminal/tmux.d.ts.map +1 -0
  180. package/dist/lib/terminal/tmux.js +528 -0
  181. package/dist/lib/terminal/tmux.js.map +1 -0
  182. package/dist/lib/terminal/types.d.ts +18 -0
  183. package/dist/lib/terminal/types.d.ts.map +1 -0
  184. package/dist/lib/terminal/types.js +5 -0
  185. package/dist/lib/terminal/types.js.map +1 -0
  186. package/lmdb.db +0 -0
  187. package/lmdb.db-lock +0 -0
  188. package/package.json +48 -0
  189. package/src/index.js +2034 -0
  190. package/src/index.ts +2295 -0
  191. package/src/lib/hetzner/actions.ts +1056 -0
  192. package/src/lib/hetzner/auth.ts +37 -0
  193. package/src/lib/hetzner/bootstrap/cloud-init.ts +394 -0
  194. package/src/lib/hetzner/bootstrap/firewall.ts +342 -0
  195. package/src/lib/hetzner/bootstrap/genesis.ts +518 -0
  196. package/src/lib/hetzner/bootstrap/index.ts +71 -0
  197. package/src/lib/hetzner/bootstrap/kernel-hardening.test.ts +230 -0
  198. package/src/lib/hetzner/bootstrap/kernel-hardening.ts +272 -0
  199. package/src/lib/hetzner/bootstrap/security-audit.ts +124 -0
  200. package/src/lib/hetzner/bootstrap/ssh-hardening.ts +192 -0
  201. package/src/lib/hetzner/client.ts +177 -0
  202. package/src/lib/hetzner/config.ts +5 -0
  203. package/src/lib/hetzner/errors.ts +371 -0
  204. package/src/lib/hetzner/index.ts +56 -0
  205. package/src/lib/hetzner/pricing.ts +422 -0
  206. package/src/lib/hetzner/schemas.ts +765 -0
  207. package/src/lib/hetzner/server-status.ts +81 -0
  208. package/src/lib/hetzner/servers.ts +568 -0
  209. package/src/lib/hetzner/ssh-keys.ts +122 -0
  210. package/src/lib/hetzner/ssh-setup.ts +218 -0
  211. package/src/lib/hetzner/types.ts +419 -0
  212. package/src/lib/hetzner/volumes.ts +229 -0
  213. package/src/lib/resources.ts +156 -0
  214. package/src/lib/ssh/flags.ts +578 -0
  215. package/src/lib/ssh/index.ts +5 -0
  216. package/src/lib/terminal/client.ts +55 -0
  217. package/src/lib/terminal/config.ts +489 -0
  218. package/src/lib/terminal/error.ts +13 -0
  219. package/src/lib/terminal/exec.ts +128 -0
  220. package/src/lib/terminal/files.ts +636 -0
  221. package/src/lib/terminal/index.ts +71 -0
  222. package/src/lib/terminal/pool.ts +662 -0
  223. package/src/lib/terminal/scp.ts +109 -0
  224. package/src/lib/terminal/tmux-exec.ts +96 -0
  225. package/src/lib/terminal/tmux.ts +711 -0
  226. package/src/lib/terminal/types.ts +19 -0
  227. package/tsconfig.json +20 -0
@@ -0,0 +1,1056 @@
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
+
8
+ import type { HetznerClient } from "./client.js";
9
+ import type {
10
+ HetznerAction,
11
+ ActionPollingOptions,
12
+ RateLimitInfo,
13
+ } from "./types.js";
14
+ import {
15
+ ActionStatus,
16
+ ActionCommand,
17
+ } from "./types.js";
18
+ import {
19
+ HetznerActionError,
20
+ HetznerTimeoutError,
21
+ isRetryableError,
22
+ calculateRetryDelay,
23
+ type ErrorHandler,
24
+ } from "./errors.js";
25
+
26
+ // ============================================================================
27
+ // Enhanced Polling Options
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Enhanced options for action polling with more granular control
32
+ */
33
+ export interface EnhancedActionPollingOptions {
34
+ /** Polling interval in milliseconds (default: 2000) */
35
+ pollInterval?: number;
36
+ /** Maximum number of polling attempts before timeout */
37
+ maxRetries?: number;
38
+ /** Maximum time to wait in milliseconds before timeout */
39
+ timeout?: number;
40
+ /** Callback fired on each progress update */
41
+ onProgress?: (action: HetznerAction) => void;
42
+ /** Callback fired when action completes successfully */
43
+ onComplete?: (action: HetznerAction) => void;
44
+ /** Callback fired when action fails */
45
+ onError?: (error: HetznerActionError, action: HetznerAction) => void;
46
+ /** Callback fired on each retry attempt */
47
+ onRetry?: (attempt: number, delay: number) => void;
48
+ /** Abort signal for cancellation */
49
+ signal?: AbortSignal;
50
+ /** Whether to use adaptive polling intervals (default: false) */
51
+ adaptive?: boolean;
52
+ /** Concurrency limit for multiple actions (default: 5) */
53
+ concurrency?: number;
54
+ }
55
+
56
+ /**
57
+ * Result of a polling operation
58
+ */
59
+ export interface PollingResult<T = HetznerAction> {
60
+ /** Whether the operation completed successfully */
61
+ success: boolean;
62
+ /** The final action state (if successful) */
63
+ action?: HetznerAction;
64
+ /** Error that occurred (if failed) */
65
+ error?: Error;
66
+ /** Number of polling attempts made */
67
+ attempts: number;
68
+ /** Total time elapsed in milliseconds */
69
+ elapsed: number;
70
+ }
71
+
72
+ /**
73
+ * Result for batch polling operations
74
+ */
75
+ export interface BatchPollingResult {
76
+ /** Map of action ID to result */
77
+ results: Map<number, PollingResult>;
78
+ /** Count of successful actions */
79
+ successful: number;
80
+ /** Count of failed actions */
81
+ failed: number;
82
+ /** Total time elapsed in milliseconds */
83
+ elapsed: number;
84
+ }
85
+
86
+ // ============================================================================
87
+ // Action Operations Class
88
+ // ============================================================================
89
+
90
+ /**
91
+ * Action operations for the Hetzner Cloud API
92
+ *
93
+ * Provides methods for managing and polling actions with enhanced
94
+ * polling capabilities including cancellation, progress tracking,
95
+ * and batch operations.
96
+ */
97
+ export class ActionOperations {
98
+ constructor(private client: HetznerClient) {}
99
+
100
+ /**
101
+ * Get a specific action by ID
102
+ *
103
+ * @param id - Action ID
104
+ * @returns Action details
105
+ */
106
+ async get(id: number): Promise<HetznerAction> {
107
+ const response = await this.client.request<{ action: HetznerAction }>(
108
+ `/actions/${id}`
109
+ );
110
+ return response.action;
111
+ }
112
+
113
+ /**
114
+ * List all actions
115
+ *
116
+ * @param options - Optional filters (status, sort, server_id, etc.)
117
+ * @returns Array of actions
118
+ */
119
+ async list(options?: {
120
+ status?: ActionStatus;
121
+ sort?: string;
122
+ page?: number;
123
+ per_page?: number;
124
+ server_id?: number;
125
+ }): Promise<HetznerAction[]> {
126
+ const params = new URLSearchParams();
127
+ if (options?.status) params.append("status", options.status);
128
+ if (options?.sort) params.append("sort", options.sort);
129
+ if (options?.page) params.append("page", options.page.toString());
130
+ if (options?.per_page)
131
+ params.append("per_page", options.per_page.toString());
132
+ if (options?.server_id)
133
+ params.append("server_id", options.server_id.toString());
134
+
135
+ const query = params.toString();
136
+ const response = await this.client.request<{ actions: HetznerAction[] }>(
137
+ `/actions${query ? `?${query}` : ""}`
138
+ );
139
+ return response.actions;
140
+ }
141
+
142
+ /**
143
+ * Poll a single action until completion (enhanced version)
144
+ *
145
+ * This is the main polling service method with full feature support:
146
+ * - Progress tracking with onProgress callback
147
+ * - Success/failure callbacks
148
+ * - Cancellation via AbortSignal
149
+ * - Adaptive polling intervals
150
+ * - Comprehensive error handling
151
+ *
152
+ * @param actionId - Action ID to poll
153
+ * @param options - Polling options
154
+ * @returns Promise resolving to the completed action
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * const action = await client.actions.poll(123, {
159
+ * onProgress: (a) => console.log(`Progress: ${a.progress}%`),
160
+ * onComplete: (a) => console.log('Done!'),
161
+ * onError: (e) => console.error('Failed:', e.message),
162
+ * timeout: 300000, // 5 minutes
163
+ * signal: abortController.signal
164
+ * });
165
+ * ```
166
+ */
167
+ async poll(
168
+ actionId: number,
169
+ options: EnhancedActionPollingOptions = {}
170
+ ): Promise<HetznerAction> {
171
+ return pollAction(this.client, actionId, options);
172
+ }
173
+
174
+ /**
175
+ * Poll multiple actions until completion
176
+ *
177
+ * Handles concurrent polling of multiple actions with optional
178
+ * concurrency limit and individual action tracking.
179
+ *
180
+ * @param actionIds - Array of action IDs to poll
181
+ * @param options - Polling options
182
+ * @returns Promise resolving to array of completed actions
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * const actions = await client.actions.pollMany([1, 2, 3], {
187
+ * onProgress: (a) => updateUI(a),
188
+ * concurrency: 3, // Poll 3 at a time
189
+ * signal: abortController.signal
190
+ * });
191
+ * ```
192
+ */
193
+ async pollMany(
194
+ actionIds: number[],
195
+ options: EnhancedActionPollingOptions = {}
196
+ ): Promise<HetznerAction[]> {
197
+ return pollActions(this.client, actionIds, options);
198
+ }
199
+
200
+ /**
201
+ * Poll multiple actions with detailed result tracking
202
+ *
203
+ * Returns a BatchPollingResult with success/failure counts and
204
+ * per-action results for better error handling.
205
+ *
206
+ * @param actionIds - Array of action IDs to poll
207
+ * @param options - Polling options
208
+ * @returns Batch polling result with detailed status
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const result = await client.actions.pollManyDetailed([1, 2, 3], {
213
+ * concurrency: 2
214
+ * });
215
+ * console.log(`Success: ${result.successful}, Failed: ${result.failed}`);
216
+ * for (const [id, r] of result.results) {
217
+ * if (r.error) console.error(`Action ${id} failed:`, r.error);
218
+ * }
219
+ * ```
220
+ */
221
+ async pollManyDetailed(
222
+ actionIds: number[],
223
+ options: EnhancedActionPollingOptions = {}
224
+ ): Promise<BatchPollingResult> {
225
+ return pollActionsDetailed(this.client, actionIds, options);
226
+ }
227
+
228
+ /**
229
+ * Wait for an action to complete (backward compatible alias)
230
+ *
231
+ * @param id - Action ID
232
+ * @param options - Polling options
233
+ * @returns Completed action
234
+ * @deprecated Use poll() instead for new code
235
+ */
236
+ async waitFor(
237
+ id: number,
238
+ options?: ActionPollingOptions
239
+ ): Promise<HetznerAction> {
240
+ return waitForAction(this.client, id, options);
241
+ }
242
+
243
+ /**
244
+ * Wait for multiple actions to complete (backward compatible alias)
245
+ *
246
+ * @param ids - Array of action IDs
247
+ * @param options - Polling options
248
+ * @returns Array of completed actions
249
+ * @deprecated Use pollMany() instead for new code
250
+ */
251
+ async waitForMany(
252
+ ids: number[],
253
+ options?: ActionPollingOptions
254
+ ): Promise<HetznerAction[]> {
255
+ return waitForMultipleActions(this.client, ids, options);
256
+ }
257
+
258
+ /**
259
+ * Batch check multiple actions (no polling, single fetch)
260
+ *
261
+ * @param ids - Array of action IDs
262
+ * @returns Map of action ID to action
263
+ */
264
+ async batchCheck(
265
+ ids: number[]
266
+ ): Promise<Map<number, HetznerAction>> {
267
+ return batchCheckActions(this.client, ids);
268
+ }
269
+ }
270
+
271
+ // ============================================================================
272
+ // Action Timeouts
273
+ // ============================================================================
274
+
275
+ /**
276
+ * Default timeouts for different action types (in milliseconds)
277
+ * Adjusted based on typical operation durations
278
+ */
279
+ export const ACTION_TIMEOUTS: Record<ActionCommand, number> = {
280
+ // Quick operations (under 1 minute)
281
+ [ActionCommand.StartServer]: 60000,
282
+ [ActionCommand.StopServer]: 60000,
283
+ [ActionCommand.RebootServer]: 120000,
284
+ [ActionCommand.Poweroff]: 60000,
285
+ [ActionCommand.ShutdownServer]: 60000,
286
+ [ActionCommand.ResetServer]: 60000,
287
+
288
+ // Medium operations (1-5 minutes)
289
+ [ActionCommand.CreateServer]: 300000,
290
+ [ActionCommand.DeleteServer]: 180000,
291
+ [ActionCommand.ChangeServerType]: 600000,
292
+ [ActionCommand.ChangeDnsPtr]: 30000,
293
+
294
+ // Long operations (5-30 minutes)
295
+ [ActionCommand.RebuildServer]: 900000,
296
+ [ActionCommand.CreateImage]: 1800000,
297
+ [ActionCommand.EnableRescue]: 120000,
298
+ [ActionCommand.DisableRescue]: 60000,
299
+
300
+ // Volume operations
301
+ [ActionCommand.CreateVolume]: 300000,
302
+ [ActionCommand.DeleteVolume]: 180000,
303
+ [ActionCommand.AttachVolume]: 120000,
304
+ [ActionCommand.DetachVolume]: 60000,
305
+ [ActionCommand.ResizeVolume]: 600000,
306
+
307
+ // Network operations
308
+ [ActionCommand.AddSubnet]: 60000,
309
+ [ActionCommand.DeleteSubnet]: 60000,
310
+ [ActionCommand.AddRoute]: 60000,
311
+ [ActionCommand.DeleteRoute]: 60000,
312
+ [ActionCommand.ChangeIpRange]: 120000,
313
+ [ActionCommand.AttachToNetwork]: 60000,
314
+ [ActionCommand.DetachFromNetwork]: 60000,
315
+
316
+ // Floating IP operations
317
+ [ActionCommand.AssignFloatingIp]: 60000,
318
+ [ActionCommand.UnassignFloatingIp]: 60000,
319
+
320
+ // Load Balancer operations
321
+ [ActionCommand.CreateLoadBalancer]: 300000,
322
+ [ActionCommand.DeleteLoadBalancer]: 180000,
323
+ [ActionCommand.AddTarget]: 60000,
324
+ [ActionCommand.RemoveTarget]: 60000,
325
+ [ActionCommand.AddService]: 60000,
326
+ [ActionCommand.UpdateService]: 60000,
327
+ [ActionCommand.DeleteService]: 60000,
328
+ [ActionCommand.LoadBalancerAttachToNetwork]: 60000,
329
+ [ActionCommand.LoadBalancerDetachFromNetwork]: 60000,
330
+ [ActionCommand.ChangeAlgorithm]: 60000,
331
+ [ActionCommand.ChangeType]: 60000,
332
+
333
+ // Certificate operations
334
+ [ActionCommand.IssueCertificate]: 600000, // Can take up to 10 minutes
335
+ [ActionCommand.RetryCertificate]: 600000,
336
+
337
+ // Firewall operations
338
+ [ActionCommand.SetFirewallRules]: 60000,
339
+ [ActionCommand.ApplyFirewall]: 120000,
340
+ [ActionCommand.RemoveFirewall]: 60000,
341
+
342
+ // Floating IP DNS
343
+ [ActionCommand.FloatingIpChangeDnsPtr]: 30000,
344
+
345
+ // Backup operations
346
+ [ActionCommand.EnableBackup]: 60000,
347
+ [ActionCommand.DisableBackup]: 60000,
348
+
349
+ // Protection operations
350
+ [ActionCommand.ChangeProtection]: 30000,
351
+ [ActionCommand.VolumeChangeProtection]: 30000,
352
+ [ActionCommand.NetworkChangeProtection]: 30000,
353
+ [ActionCommand.FloatingIpChangeProtection]: 30000,
354
+ [ActionCommand.LoadBalancerChangeProtection]: 30000,
355
+ [ActionCommand.FirewallChangeProtection]: 30000,
356
+ [ActionCommand.ImageChangeProtection]: 30000,
357
+
358
+ // Other operations
359
+ [ActionCommand.ChangeAliasIps]: 60000,
360
+ };
361
+
362
+ /**
363
+ * Get default timeout for an action command
364
+ */
365
+ export function getActionTimeout(command: ActionCommand): number {
366
+ return ACTION_TIMEOUTS[command] || 300000; // Default 5 minutes
367
+ }
368
+
369
+ // ============================================================================
370
+ // Enhanced Action Polling Service
371
+ // ============================================================================
372
+
373
+ /**
374
+ * Default polling options
375
+ */
376
+ const DEFAULT_POLLING_OPTIONS: Omit<Required<EnhancedActionPollingOptions>, 'signal'> & { signal: AbortSignal | undefined } = {
377
+ pollInterval: 2000,
378
+ maxRetries: 60,
379
+ timeout: 300000,
380
+ onProgress: () => {},
381
+ onComplete: () => {},
382
+ onError: () => {},
383
+ onRetry: () => {},
384
+ signal: undefined,
385
+ adaptive: false,
386
+ concurrency: 5,
387
+ };
388
+
389
+ /**
390
+ * Poll for an action to complete with full feature support
391
+ *
392
+ * This is the core polling service function that provides:
393
+ * - Progress tracking via onProgress callback
394
+ * - Success/error notification via onComplete/onError
395
+ * - Cancellation support via AbortSignal
396
+ * - Adaptive polling intervals
397
+ * - Retry logic with exponential backoff
398
+ *
399
+ * @param client - Hetzner API client
400
+ * @param actionId - Action ID to poll
401
+ * @param options - Polling options
402
+ * @returns Completed action
403
+ * @throws {HetznerActionError} If action fails
404
+ * @throws {HetznerTimeoutError} If action times out
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * const action = await pollAction(client, 123, {
409
+ * onProgress: (a) => console.log(`Progress: ${a.progress}%`),
410
+ * onComplete: (a) => console.log('Completed:', a.command),
411
+ * onError: (e) => console.error('Failed:', e.message),
412
+ * timeout: 60000,
413
+ * signal: abortSignal
414
+ * });
415
+ * ```
416
+ */
417
+ export async function pollAction(
418
+ client: HetznerClient,
419
+ actionId: number,
420
+ options: EnhancedActionPollingOptions = {}
421
+ ): Promise<HetznerAction> {
422
+ const opts = { ...DEFAULT_POLLING_OPTIONS, ...options };
423
+ const startTime = Date.now();
424
+ let lastProgress = 0;
425
+ let lastError: Error | null = null;
426
+ let attempt = 0;
427
+
428
+ // Check if already aborted
429
+ if (opts.signal?.aborted) {
430
+ throw new Error("Polling aborted before start");
431
+ }
432
+
433
+ // Set up abort listener
434
+ const abortListener = () => {
435
+ throw new Error("Polling aborted");
436
+ };
437
+ opts.signal?.addEventListener("abort", abortListener);
438
+
439
+ try {
440
+ while (attempt < opts.maxRetries) {
441
+ // Check timeout
442
+ const elapsed = Date.now() - startTime;
443
+ if (elapsed > opts.timeout) {
444
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
445
+ }
446
+
447
+ try {
448
+ const response = await client.request<{ action: HetznerAction }>(
449
+ `/actions/${actionId}`
450
+ );
451
+ const action = response.action;
452
+ attempt++;
453
+
454
+ // Notify progress callback on any change
455
+ if (action.progress !== lastProgress) {
456
+ lastProgress = action.progress;
457
+ opts.onProgress(action);
458
+ }
459
+
460
+ // Check if action completed successfully
461
+ if (action.status === "success") {
462
+ opts.onComplete(action);
463
+ return action;
464
+ }
465
+
466
+ // Check if action failed
467
+ if (action.status === "error") {
468
+ const error = new HetznerActionError(
469
+ action.error!,
470
+ actionId
471
+ );
472
+ opts.onError(error, action);
473
+ throw error;
474
+ }
475
+
476
+ // Still running - calculate wait time
477
+ let waitTime = opts.pollInterval;
478
+ if (opts.adaptive) {
479
+ waitTime = getAdaptivePollInterval(action.progress);
480
+ }
481
+
482
+ await sleep(waitTime);
483
+
484
+ } catch (error) {
485
+ // Check if this is a retryable error
486
+ if (isRetryableError(error)) {
487
+ const delay = calculateRetryDelay(attempt);
488
+ opts.onRetry(attempt, delay);
489
+
490
+ if (opts.signal?.aborted) {
491
+ throw new Error("Polling aborted during retry");
492
+ }
493
+
494
+ await sleep(delay);
495
+ continue;
496
+ }
497
+
498
+ // Non-retryable error - throw immediately
499
+ throw error;
500
+ }
501
+ }
502
+
503
+ // Max retries exceeded
504
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
505
+
506
+ } finally {
507
+ opts.signal?.removeEventListener("abort", abortListener);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Poll multiple actions concurrently
513
+ *
514
+ * Manages polling of multiple actions with optional concurrency limit.
515
+ * Each action is polled independently with shared callbacks.
516
+ *
517
+ * @param client - Hetzner API client
518
+ * @param actionIds - Array of action IDs to poll
519
+ * @param options - Polling options
520
+ * @returns Array of completed actions in same order as input
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const actions = await pollActions(client, [1, 2, 3], {
525
+ * onProgress: (a) => console.log(`Action ${a.id}: ${a.progress}%`),
526
+ * onComplete: (a) => console.log(`Action ${a.id} complete`),
527
+ * concurrency: 3 // Poll 3 actions at a time
528
+ * });
529
+ * ```
530
+ */
531
+ export async function pollActions(
532
+ client: HetznerClient,
533
+ actionIds: number[],
534
+ options: EnhancedActionPollingOptions = {}
535
+ ): Promise<HetznerAction[]> {
536
+ const concurrency = options.concurrency ?? DEFAULT_POLLING_OPTIONS.concurrency;
537
+
538
+ // If concurrency is high enough, poll all at once
539
+ if (concurrency >= actionIds.length) {
540
+ const promises = actionIds.map((id) =>
541
+ pollAction(client, id, options)
542
+ );
543
+ return Promise.all(promises);
544
+ }
545
+
546
+ // Otherwise, process in batches with concurrency limit
547
+ const results: HetznerAction[] = new Array(actionIds.length);
548
+ let currentIndex = 0;
549
+
550
+ const processBatch = async (): Promise<void> => {
551
+ while (currentIndex < actionIds.length) {
552
+ const batchStart = currentIndex;
553
+ const batchEnd = Math.min(currentIndex + concurrency, actionIds.length);
554
+ const batchIds = actionIds.slice(batchStart, batchEnd);
555
+ currentIndex = batchEnd;
556
+
557
+ const batchResults = await Promise.all(
558
+ batchIds.map((id, i) => pollAction(client, id, options))
559
+ );
560
+
561
+ // Store results in correct positions
562
+ batchResults.forEach((result, i) => {
563
+ results[batchStart + i] = result;
564
+ });
565
+ }
566
+ };
567
+
568
+ // Process batches sequentially but actions within each batch concurrently
569
+ await processBatch();
570
+ return results;
571
+ }
572
+
573
+ /**
574
+ * Poll multiple actions with detailed result tracking
575
+ *
576
+ * Similar to pollActions but returns a BatchPollingResult with
577
+ * success/failure counts and per-action results for better
578
+ * error handling and monitoring.
579
+ *
580
+ * @param client - Hetzner API client
581
+ * @param actionIds - Array of action IDs to poll
582
+ * @param options - Polling options
583
+ * @returns Batch polling result with detailed status
584
+ *
585
+ * @example
586
+ * ```typescript
587
+ * const result = await pollActionsDetailed(client, [1, 2, 3], {
588
+ * onProgress: (a) => updateProgressBar(a)
589
+ * });
590
+ *
591
+ * console.log(`Completed: ${result.successful}/${actionIds.length}`);
592
+ * for (const [id, r] of result.results) {
593
+ * if (r.error) {
594
+ * console.error(`Action ${id} failed:`, r.error.message);
595
+ * }
596
+ * }
597
+ * ```
598
+ */
599
+ export async function pollActionsDetailed(
600
+ client: HetznerClient,
601
+ actionIds: number[],
602
+ options: EnhancedActionPollingOptions = {}
603
+ ): Promise<BatchPollingResult> {
604
+ const startTime = Date.now();
605
+ const results = new Map<number, PollingResult>();
606
+ let successful = 0;
607
+ let failed = 0;
608
+
609
+ // Wrap callbacks to track results
610
+ const wrappedOptions: EnhancedActionPollingOptions = {
611
+ ...options,
612
+ onComplete: (action) => {
613
+ results.set(action.id, {
614
+ success: true,
615
+ action,
616
+ attempts: 0,
617
+ elapsed: Date.now() - startTime,
618
+ });
619
+ successful++;
620
+ options.onComplete?.(action);
621
+ },
622
+ onError: (error, action) => {
623
+ results.set(action.id, {
624
+ success: false,
625
+ action,
626
+ error,
627
+ attempts: 0,
628
+ elapsed: Date.now() - startTime,
629
+ });
630
+ failed++;
631
+ options.onError?.(error, action);
632
+ },
633
+ };
634
+
635
+ try {
636
+ // Poll all actions
637
+ await pollActions(client, actionIds, wrappedOptions);
638
+ } catch (error) {
639
+ // Some actions may have failed, but we still return results
640
+ console.warn("Some actions failed during batch polling:", error);
641
+ }
642
+
643
+ return {
644
+ results,
645
+ successful,
646
+ failed,
647
+ elapsed: Date.now() - startTime,
648
+ };
649
+ }
650
+
651
+ // ============================================================================
652
+ // Backward Compatible Polling Functions
653
+ // ============================================================================
654
+
655
+ /**
656
+ * Poll for an action to complete (backward compatible)
657
+ *
658
+ * @deprecated Use pollAction() instead for new code
659
+ */
660
+ export async function waitForAction(
661
+ client: HetznerClient,
662
+ actionId: number,
663
+ options: ActionPollingOptions = {}
664
+ ): Promise<HetznerAction> {
665
+ const opts = { ...DEFAULT_POLLING_OPTIONS, ...options };
666
+ const startTime = Date.now();
667
+ let lastProgress = 0;
668
+ let lastError: Error | null = null;
669
+
670
+ for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
671
+ // Check timeout
672
+ if (Date.now() - startTime > opts.timeout) {
673
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
674
+ }
675
+
676
+ try {
677
+ const action = await client.request<{ action: HetznerAction }>(
678
+ `/actions/${actionId}`
679
+ );
680
+
681
+ // Notify progress callback
682
+ if (action.action.progress !== lastProgress) {
683
+ lastProgress = action.action.progress;
684
+ opts.onProgress(action.action);
685
+ }
686
+
687
+ // Check if action is complete
688
+ if (action.action.status === "success") {
689
+ return action.action;
690
+ }
691
+
692
+ if (action.action.status === "error") {
693
+ throw new HetznerActionError(
694
+ action.action.error!,
695
+ actionId
696
+ );
697
+ }
698
+
699
+ // Still running - wait before next poll
700
+ await sleep(opts.pollInterval);
701
+
702
+ } catch (error) {
703
+ // Check if error is retryable
704
+ if (isRetryableError(error)) {
705
+ const delay = calculateRetryDelay(attempt);
706
+ console.warn(
707
+ `Retrying action ${actionId} after ${delay}ms (attempt ${attempt + 1}/${opts.maxRetries})`
708
+ );
709
+ await sleep(delay);
710
+ continue;
711
+ }
712
+
713
+ // Non-retryable error - throw immediately
714
+ throw error;
715
+ }
716
+ }
717
+
718
+ // Max retries exceeded
719
+ throw new HetznerTimeoutError(actionId, opts.timeout, lastProgress);
720
+ }
721
+
722
+ /**
723
+ * Poll for multiple actions concurrently (backward compatible)
724
+ *
725
+ * @deprecated Use pollActions() instead for new code
726
+ */
727
+ export async function waitForMultipleActions(
728
+ client: HetznerClient,
729
+ actionIds: number[],
730
+ options: ActionPollingOptions = {}
731
+ ): Promise<HetznerAction[]> {
732
+ const promises = actionIds.map((id) => waitForAction(client, id, options));
733
+ return Promise.all(promises);
734
+ }
735
+
736
+ /**
737
+ * Poll for multiple actions with concurrency limit (backward compatible)
738
+ *
739
+ * @deprecated Use pollActions() with concurrency option instead
740
+ */
741
+ export async function waitForMultipleActionsWithLimit(
742
+ client: HetznerClient,
743
+ actionIds: number[],
744
+ concurrency: number = 5,
745
+ options: ActionPollingOptions = {}
746
+ ): Promise<HetznerAction[]> {
747
+ const results: HetznerAction[] = [];
748
+ const chunks: number[][] = [];
749
+
750
+ // Split action IDs into chunks
751
+ for (let i = 0; i < actionIds.length; i += concurrency) {
752
+ chunks.push(actionIds.slice(i, i + concurrency));
753
+ }
754
+
755
+ // Process each chunk sequentially
756
+ for (const chunk of chunks) {
757
+ const chunkResults = await waitForMultipleActions(client, chunk, options);
758
+ results.push(...chunkResults);
759
+ }
760
+
761
+ return results;
762
+ }
763
+
764
+ /**
765
+ * Batch check multiple actions in a single API call
766
+ *
767
+ * Uses the /actions?id=42&id=43 endpoint to fetch multiple actions at once
768
+ *
769
+ * @param client - Hetzner API client
770
+ * @param actionIds - Array of action IDs to check
771
+ * @returns Map of action ID to action
772
+ */
773
+ export async function batchCheckActions(
774
+ client: HetznerClient,
775
+ actionIds: number[]
776
+ ): Promise<Map<number, HetznerAction>> {
777
+ const MAX_BATCH_SIZE = 50; // API limit for ID parameter
778
+ const results = new Map<number, HetznerAction>();
779
+
780
+ // Process in batches
781
+ for (let i = 0; i < actionIds.length; i += MAX_BATCH_SIZE) {
782
+ const batch = actionIds.slice(i, i + MAX_BATCH_SIZE);
783
+ const idParams = batch.map((id) => `id=${id}`).join("&");
784
+
785
+ const response = await client.request<{ actions: HetznerAction[] }>(
786
+ `/actions?${idParams}`
787
+ );
788
+
789
+ for (const action of response.actions) {
790
+ results.set(action.id, action);
791
+ }
792
+ }
793
+
794
+ return results;
795
+ }
796
+
797
+ // ============================================================================
798
+ // Action Status Utilities
799
+ // ============================================================================
800
+
801
+ /**
802
+ * Check if an action is running
803
+ */
804
+ export function isActionRunning(action: HetznerAction): boolean {
805
+ return action.status === "running";
806
+ }
807
+
808
+ /**
809
+ * Check if an action completed successfully
810
+ */
811
+ export function isActionSuccess(action: HetznerAction): boolean {
812
+ return action.status === "success";
813
+ }
814
+
815
+ /**
816
+ * Check if an action failed
817
+ */
818
+ export function isActionError(action: HetznerAction): boolean {
819
+ return action.status === "error";
820
+ }
821
+
822
+ /**
823
+ * Format action progress for display
824
+ */
825
+ export function formatActionProgress(action: HetznerAction): string {
826
+ const command = action.command.replace(/_/g, " ");
827
+ const status = action.status.toUpperCase();
828
+ return `${status}: ${command} (${action.progress}%)`;
829
+ }
830
+
831
+ /**
832
+ * Get human-readable action description
833
+ */
834
+ export function getActionDescription(command: ActionCommand | string): string {
835
+ const descriptions: Record<ActionCommand, string> = {
836
+ [ActionCommand.CreateServer]: "Creating server",
837
+ [ActionCommand.DeleteServer]: "Deleting server",
838
+ [ActionCommand.StartServer]: "Starting server",
839
+ [ActionCommand.StopServer]: "Stopping server",
840
+ [ActionCommand.RebootServer]: "Rebooting server",
841
+ [ActionCommand.ResetServer]: "Resetting server",
842
+ [ActionCommand.ShutdownServer]: "Shutting down server",
843
+ [ActionCommand.Poweroff]: "Cutting power to server",
844
+ [ActionCommand.ChangeServerType]: "Changing server type",
845
+ [ActionCommand.RebuildServer]: "Rebuilding server",
846
+ [ActionCommand.EnableBackup]: "Enabling backups",
847
+ [ActionCommand.DisableBackup]: "Disabling backups",
848
+ [ActionCommand.CreateImage]: "Creating image",
849
+ [ActionCommand.ChangeDnsPtr]: "Changing reverse DNS",
850
+ [ActionCommand.AttachToNetwork]: "Attaching to network",
851
+ [ActionCommand.DetachFromNetwork]: "Detaching from network",
852
+ [ActionCommand.ChangeAliasIps]: "Changing alias IPs",
853
+ [ActionCommand.EnableRescue]: "Enabling rescue mode",
854
+ [ActionCommand.DisableRescue]: "Disabling rescue mode",
855
+ [ActionCommand.ChangeProtection]: "Changing protection",
856
+ [ActionCommand.CreateVolume]: "Creating volume",
857
+ [ActionCommand.DeleteVolume]: "Deleting volume",
858
+ [ActionCommand.AttachVolume]: "Attaching volume",
859
+ [ActionCommand.DetachVolume]: "Detaching volume",
860
+ [ActionCommand.ResizeVolume]: "Resizing volume",
861
+ [ActionCommand.VolumeChangeProtection]: "Changing volume protection",
862
+ [ActionCommand.AddSubnet]: "Adding subnet",
863
+ [ActionCommand.DeleteSubnet]: "Deleting subnet",
864
+ [ActionCommand.AddRoute]: "Adding route",
865
+ [ActionCommand.DeleteRoute]: "Deleting route",
866
+ [ActionCommand.ChangeIpRange]: "Changing IP range",
867
+ [ActionCommand.NetworkChangeProtection]: "Changing network protection",
868
+ [ActionCommand.AssignFloatingIp]: "Assigning floating IP",
869
+ [ActionCommand.UnassignFloatingIp]: "Unassigning floating IP",
870
+ [ActionCommand.FloatingIpChangeDnsPtr]: "Changing floating IP DNS",
871
+ [ActionCommand.FloatingIpChangeProtection]: "Changing floating IP protection",
872
+ [ActionCommand.CreateLoadBalancer]: "Creating load balancer",
873
+ [ActionCommand.DeleteLoadBalancer]: "Deleting load balancer",
874
+ [ActionCommand.AddTarget]: "Adding target to load balancer",
875
+ [ActionCommand.RemoveTarget]: "Removing target from load balancer",
876
+ [ActionCommand.AddService]: "Adding service to load balancer",
877
+ [ActionCommand.UpdateService]: "Updating load balancer service",
878
+ [ActionCommand.DeleteService]: "Deleting load balancer service",
879
+ [ActionCommand.LoadBalancerAttachToNetwork]: "Attaching load balancer to network",
880
+ [ActionCommand.LoadBalancerDetachFromNetwork]: "Detaching load balancer from network",
881
+ [ActionCommand.ChangeAlgorithm]: "Changing load balancer algorithm",
882
+ [ActionCommand.ChangeType]: "Changing load balancer type",
883
+ [ActionCommand.LoadBalancerChangeProtection]: "Changing load balancer protection",
884
+ [ActionCommand.IssueCertificate]: "Issuing certificate",
885
+ [ActionCommand.RetryCertificate]: "Retrying certificate",
886
+ [ActionCommand.SetFirewallRules]: "Setting firewall rules",
887
+ [ActionCommand.ApplyFirewall]: "Applying firewall",
888
+ [ActionCommand.RemoveFirewall]: "Removing firewall",
889
+ [ActionCommand.FirewallChangeProtection]: "Changing firewall protection",
890
+ [ActionCommand.ImageChangeProtection]: "Changing image protection",
891
+ };
892
+
893
+ return (descriptions as Record<string, string>)[command] || command.replace(/_/g, " ");
894
+ }
895
+
896
+ // ============================================================================
897
+ // Adaptive Polling
898
+ // ============================================================================
899
+
900
+ /**
901
+ * Get adaptive polling interval based on operation type
902
+ */
903
+ export function getPollInterval(command: ActionCommand): number {
904
+ const intervals: Partial<Record<ActionCommand, number>> = {
905
+ // Quick operations - poll frequently
906
+ [ActionCommand.StartServer]: 5000,
907
+ [ActionCommand.StopServer]: 5000,
908
+ [ActionCommand.RebootServer]: 10000,
909
+ [ActionCommand.Poweroff]: 5000,
910
+ [ActionCommand.ShutdownServer]: 5000,
911
+
912
+ // Medium operations
913
+ [ActionCommand.CreateServer]: 15000,
914
+ [ActionCommand.CreateVolume]: 10000,
915
+ [ActionCommand.AttachVolume]: 8000,
916
+ [ActionCommand.DetachVolume]: 5000,
917
+
918
+ // Long operations - poll less frequently
919
+ [ActionCommand.RebuildServer]: 30000,
920
+ [ActionCommand.CreateImage]: 60000,
921
+ [ActionCommand.IssueCertificate]: 30000,
922
+ };
923
+
924
+ return intervals[command] || 10000; // Default 10 seconds
925
+ }
926
+
927
+ /**
928
+ * Get adaptive polling interval based on action progress
929
+ */
930
+ export function getAdaptivePollInterval(progress: number): number {
931
+ if (progress < 10) return 2000; // Quick progress at start
932
+ if (progress < 50) return 5000; // Medium progress
933
+ if (progress < 90) return 10000; // Slowing down
934
+ return 15000; // Near completion
935
+ }
936
+
937
+ /**
938
+ * Poll with adaptive intervals (backward compatible)
939
+ *
940
+ * @deprecated Use pollAction() with adaptive: true option instead
941
+ */
942
+ export async function waitForActionAdaptive(
943
+ client: HetznerClient,
944
+ actionId: number,
945
+ command: ActionCommand,
946
+ options: ActionPollingOptions = {}
947
+ ): Promise<HetznerAction> {
948
+ const startTime = Date.now();
949
+ const timeout = options.timeout ?? getActionTimeout(command);
950
+ let lastProgress = -1;
951
+
952
+ while (Date.now() - startTime < timeout) {
953
+ const action = await client.request<{ action: HetznerAction }>(
954
+ `/actions/${actionId}`
955
+ );
956
+
957
+ // Notify progress callback
958
+ if (action.action.progress !== lastProgress) {
959
+ lastProgress = action.action.progress;
960
+ options.onProgress?.(action.action);
961
+ }
962
+
963
+ if (action.action.status === "success") {
964
+ return action.action;
965
+ }
966
+
967
+ if (action.action.status === "error") {
968
+ throw new HetznerActionError(action.action.error!, actionId);
969
+ }
970
+
971
+ // Adaptive polling based on progress
972
+ const interval = getAdaptivePollInterval(action.action.progress);
973
+ await sleep(interval);
974
+ }
975
+
976
+ throw new HetznerTimeoutError(actionId, timeout, lastProgress);
977
+ }
978
+
979
+ // ============================================================================
980
+ // Rate Limit Handling
981
+ // ============================================================================
982
+
983
+ /**
984
+ * Parse rate limit headers from response
985
+ */
986
+ export function parseRateLimitHeaders(headers: Headers): RateLimitInfo | null {
987
+ const limit = headers.get("RateLimit-Limit");
988
+ const remaining = headers.get("RateLimit-Remaining");
989
+ const reset = headers.get("RateLimit-Reset");
990
+
991
+ if (!limit || !remaining || !reset) {
992
+ return null;
993
+ }
994
+
995
+ return {
996
+ limit: parseInt(limit, 10),
997
+ remaining: parseInt(remaining, 10),
998
+ reset: parseInt(reset, 10),
999
+ };
1000
+ }
1001
+
1002
+ /**
1003
+ * Check if rate limit is low (should warn user)
1004
+ */
1005
+ export function isRateLimitLow(info: RateLimitInfo, threshold: number = 100): boolean {
1006
+ return info.remaining < threshold;
1007
+ }
1008
+
1009
+ /**
1010
+ * Get human-readable rate limit status
1011
+ */
1012
+ export function formatRateLimitStatus(info: RateLimitInfo): string {
1013
+ const resetDate = new Date(info.reset * 1000);
1014
+ const remaining = info.remaining;
1015
+ const limit = info.limit;
1016
+ const percentage = ((remaining / limit) * 100).toFixed(1);
1017
+
1018
+ return `${remaining}/${limit} (${percentage}%) - resets at ${resetDate.toISOString()}`;
1019
+ }
1020
+
1021
+ /**
1022
+ * Wait for rate limit to reset
1023
+ */
1024
+ export async function waitForRateLimitReset(info: RateLimitInfo): Promise<void> {
1025
+ const resetTime = info.reset * 1000;
1026
+ const now = Date.now();
1027
+ const waitTime = Math.max(0, resetTime - now);
1028
+
1029
+ if (waitTime > 0) {
1030
+ const waitSeconds = Math.ceil(waitTime / 1000);
1031
+ console.log(`Rate limit exhausted. Waiting ${waitSeconds}s for reset...`);
1032
+ await sleep(waitTime);
1033
+ }
1034
+ }
1035
+
1036
+ // ============================================================================
1037
+ // Utilities
1038
+ // ============================================================================
1039
+
1040
+ /**
1041
+ * Sleep for specified milliseconds
1042
+ */
1043
+ function sleep(ms: number): Promise<void> {
1044
+ return new Promise((resolve) => setTimeout(resolve, ms));
1045
+ }
1046
+
1047
+ /**
1048
+ * Create a progress logger for actions
1049
+ */
1050
+ export function createProgressLogger(prefix: string = "Action") {
1051
+ return (action: HetznerAction) => {
1052
+ console.log(
1053
+ `[${prefix}] ${getActionDescription(action.command)}: ${action.progress}%`
1054
+ );
1055
+ };
1056
+ }