@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/config.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hetzner Cloud API configuration
3
+ */
4
+ export const HETZNER_API_BASE = "https://api.hetzner.cloud/v1";
5
+ //# sourceMappingURL=config.js.map
package/config.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hetzner Cloud API configuration
3
+ */
4
+
5
+ export const HETZNER_API_BASE = "https://api.hetzner.cloud/v1";
package/errors.js ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Hetzner Cloud API error types and utilities
3
+ */
4
+ // ============================================================================
5
+ // Error Codes
6
+ // ============================================================================
7
+ /**
8
+ * Hetzner API error codes
9
+ * @see https://docs.hetzner.cloud/#errors
10
+ */
11
+ export var HetznerErrorCode;
12
+ (function (HetznerErrorCode) {
13
+ // Authentication errors
14
+ HetznerErrorCode["Unauthorized"] = "unauthorized";
15
+ HetznerErrorCode["InvalidInput"] = "invalid_input";
16
+ HetznerErrorCode["JSONError"] = "json_error";
17
+ HetznerErrorCode["Forbidden"] = "forbidden";
18
+ // Resource errors
19
+ HetznerErrorCode["NotFound"] = "not_found";
20
+ HetznerErrorCode["ResourceLocked"] = "locked";
21
+ HetznerErrorCode["ResourceLimitExceeded"] = "resource_limit_exceeded";
22
+ HetznerErrorCode["UniquenessError"] = "uniqueness_error";
23
+ // Rate limiting
24
+ HetznerErrorCode["RateLimitExceeded"] = "rate_limit_exceeded";
25
+ // Conflict errors
26
+ HetznerErrorCode["Conflict"] = "conflict";
27
+ HetznerErrorCode["ServiceError"] = "service_error";
28
+ // Server-specific errors
29
+ HetznerErrorCode["ServerNotStopped"] = "server_not_stopped";
30
+ HetznerErrorCode["ServerAlreadyStopped"] = "server_already_stopped";
31
+ HetznerErrorCode["InvalidServerType"] = "invalid_server_type";
32
+ // IP/network errors
33
+ HetznerErrorCode["IpNotOwned"] = "ip_not_owned";
34
+ HetznerErrorCode["IpAlreadyAssigned"] = "ip_already_assigned";
35
+ // Volume errors
36
+ HetznerErrorCode["VolumeAlreadyAttached"] = "volume_already_attached";
37
+ HetznerErrorCode["VolumeSizeNotMultiple"] = "volume_size_not_multiple";
38
+ // Firewall errors
39
+ HetznerErrorCode["FirewallInUse"] = "firewall_in_use";
40
+ // Certificate errors
41
+ HetznerErrorCode["CertificateValidationFailed"] = "certificate_validation_failed";
42
+ HetznerErrorCode["CertificatePending"] = "certificate_pending";
43
+ })(HetznerErrorCode || (HetznerErrorCode = {}));
44
+ // ============================================================================
45
+ // Error Classes
46
+ // ============================================================================
47
+ /**
48
+ * Base Hetzner API error
49
+ */
50
+ export class HetznerAPIError extends Error {
51
+ code;
52
+ details;
53
+ constructor(message, code, details) {
54
+ super(message);
55
+ this.code = code;
56
+ this.details = details;
57
+ this.name = "HetznerAPIError";
58
+ }
59
+ }
60
+ /**
61
+ * Authentication error (401)
62
+ */
63
+ export class HetznerUnauthorizedError extends HetznerAPIError {
64
+ constructor(message = "Unauthorized: Invalid API token") {
65
+ super(message, HetznerErrorCode.Unauthorized);
66
+ this.name = "HetznerUnauthorizedError";
67
+ }
68
+ }
69
+ /**
70
+ * Forbidden error (403)
71
+ */
72
+ export class HetznerForbiddenError extends HetznerAPIError {
73
+ constructor(message = "Forbidden: Insufficient permissions") {
74
+ super(message, HetznerErrorCode.Forbidden);
75
+ this.name = "HetznerForbiddenError";
76
+ }
77
+ }
78
+ /**
79
+ * Resource not found error (404)
80
+ */
81
+ export class HetznerNotFoundError extends HetznerAPIError {
82
+ constructor(resource, id) {
83
+ super(`${resource} with ID ${id} not found`, HetznerErrorCode.NotFound, { resource, id });
84
+ this.name = "HetznerNotFoundError";
85
+ }
86
+ }
87
+ /**
88
+ * Rate limit exceeded error (429)
89
+ */
90
+ export class HetznerRateLimitError extends HetznerAPIError {
91
+ rateLimitInfo;
92
+ constructor(message = "Rate limit exceeded", rateLimitInfo) {
93
+ super(message, HetznerErrorCode.RateLimitExceeded, rateLimitInfo);
94
+ this.rateLimitInfo = rateLimitInfo;
95
+ this.name = "HetznerRateLimitError";
96
+ }
97
+ /**
98
+ * Get the number of milliseconds until the rate limit resets
99
+ */
100
+ get resetInMs() {
101
+ if (!this.rateLimitInfo)
102
+ return 60000; // Default to 1 minute
103
+ return Math.max(0, this.rateLimitInfo.reset * 1000 - Date.now());
104
+ }
105
+ /**
106
+ * Get a human-readable reset time
107
+ */
108
+ get resetTime() {
109
+ if (!this.rateLimitInfo)
110
+ return "unknown";
111
+ return new Date(this.rateLimitInfo.reset * 1000).toISOString();
112
+ }
113
+ }
114
+ /**
115
+ * Resource locked error
116
+ */
117
+ export class HetznerResourceLockedError extends HetznerAPIError {
118
+ actionInProgress;
119
+ constructor(resource, id, actionInProgress) {
120
+ super(`${resource} ${id} is locked${actionInProgress ? ` by ${actionInProgress}` : ""}`, HetznerErrorCode.ResourceLocked, { resource, id, actionInProgress });
121
+ this.actionInProgress = actionInProgress;
122
+ this.name = "HetznerResourceLockedError";
123
+ }
124
+ }
125
+ /**
126
+ * Resource limit exceeded error
127
+ */
128
+ export class HetznerResourceLimitError extends HetznerAPIError {
129
+ constructor(resource, limit) {
130
+ super(`Resource limit exceeded: ${resource} (limit: ${limit})`, HetznerErrorCode.ResourceLimitExceeded, { resource, limit });
131
+ this.name = "HetznerResourceLimitError";
132
+ }
133
+ }
134
+ /**
135
+ * Invalid input error
136
+ */
137
+ export class HetznerInvalidInputError extends HetznerAPIError {
138
+ fields;
139
+ constructor(message, fields) {
140
+ super(message, HetznerErrorCode.InvalidInput, { fields });
141
+ this.fields = fields;
142
+ this.name = "HetznerInvalidInputError";
143
+ }
144
+ }
145
+ /**
146
+ * Conflict error
147
+ */
148
+ export class HetznerConflictError extends HetznerAPIError {
149
+ constructor(message, details) {
150
+ super(message, HetznerErrorCode.Conflict, details);
151
+ this.name = "HetznerConflictError";
152
+ }
153
+ }
154
+ /**
155
+ * Service error (5xx)
156
+ */
157
+ export class HetznerServiceError extends HetznerAPIError {
158
+ statusCode;
159
+ constructor(message, statusCode) {
160
+ super(message, HetznerErrorCode.ServiceError, { statusCode });
161
+ this.statusCode = statusCode;
162
+ this.name = "HetznerServiceError";
163
+ }
164
+ }
165
+ /**
166
+ * Action failed error
167
+ */
168
+ export class HetznerActionError extends HetznerAPIError {
169
+ actionError;
170
+ actionId;
171
+ constructor(actionError, actionId) {
172
+ super(`Action ${actionId} failed: ${actionError.code} - ${actionError.message}`, actionError.code, { actionError, actionId });
173
+ this.actionError = actionError;
174
+ this.actionId = actionId;
175
+ this.name = "HetznerActionError";
176
+ }
177
+ }
178
+ /**
179
+ * Timeout error for action polling
180
+ */
181
+ export class HetznerTimeoutError extends HetznerAPIError {
182
+ lastProgress;
183
+ constructor(actionId, timeout, lastProgress) {
184
+ super(`Action ${actionId} timed out after ${timeout}ms (last progress: ${lastProgress}%)`, "timeout", { actionId, timeout, lastProgress });
185
+ this.lastProgress = lastProgress;
186
+ this.name = "HetznerTimeoutError";
187
+ }
188
+ }
189
+ // ============================================================================
190
+ // Error Factory
191
+ // ============================================================================
192
+ /**
193
+ * Parse Hetzner API error response and create appropriate error
194
+ */
195
+ export function createHetznerError(statusCode, body) {
196
+ const error = body.error;
197
+ if (!error) {
198
+ return new HetznerServiceError(`HTTP ${statusCode}: ${JSON.stringify(body)}`, statusCode);
199
+ }
200
+ switch (statusCode) {
201
+ case 401:
202
+ return new HetznerUnauthorizedError(error.message);
203
+ case 403:
204
+ return new HetznerForbiddenError(error.message);
205
+ case 404:
206
+ return new HetznerNotFoundError("resource", "unknown");
207
+ case 429:
208
+ return new HetznerRateLimitError(error.message);
209
+ case 400:
210
+ if (error.code === HetznerErrorCode.ResourceLocked) {
211
+ return new HetznerResourceLockedError("resource", "unknown", error.message);
212
+ }
213
+ if (error.code === HetznerErrorCode.InvalidInput) {
214
+ return new HetznerInvalidInputError(error.message);
215
+ }
216
+ return new HetznerInvalidInputError(error.message);
217
+ case 409:
218
+ return new HetznerConflictError(error.message, error.details);
219
+ default:
220
+ if (statusCode >= 500) {
221
+ return new HetznerServiceError(error.message, statusCode);
222
+ }
223
+ return new HetznerAPIError(error.message, error.code, error.details);
224
+ }
225
+ }
226
+ /**
227
+ * Check if an error is retryable
228
+ */
229
+ export function isRetryableError(error) {
230
+ if (error instanceof HetznerRateLimitError)
231
+ return true;
232
+ if (error instanceof HetznerResourceLockedError)
233
+ return true;
234
+ if (error instanceof HetznerServiceError)
235
+ return true;
236
+ if (error instanceof HetznerConflictError)
237
+ return true;
238
+ return false;
239
+ }
240
+ /**
241
+ * Check if an error is a rate limit error
242
+ */
243
+ export function isRateLimitError(error) {
244
+ return error instanceof HetznerRateLimitError;
245
+ }
246
+ /**
247
+ * Check if an error is a resource locked error
248
+ */
249
+ export function isResourceLockedError(error) {
250
+ return error instanceof HetznerResourceLockedError;
251
+ }
252
+ /**
253
+ * Calculate retry delay with exponential backoff
254
+ */
255
+ export function calculateRetryDelay(attempt, baseDelay = 1000, maxDelay = 60000) {
256
+ const delay = baseDelay * Math.pow(2, attempt);
257
+ // Add jitter (±25%)
258
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
259
+ return Math.min(maxDelay, delay + jitter);
260
+ }
261
+ /**
262
+ * Default error handler that logs to console
263
+ */
264
+ export function defaultErrorHandler(error) {
265
+ console.error(`[Hetzner API Error] ${error.name}: ${error.message}`);
266
+ if (error.details) {
267
+ console.error("Details:", error.details);
268
+ }
269
+ }
270
+ //# sourceMappingURL=errors.js.map
package/errors.ts ADDED
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Hetzner Cloud API error types and utilities
3
+ */
4
+
5
+ import type { RateLimitInfo, ActionError } from "./types.js";
6
+
7
+ // Re-export ActionError for convenience
8
+ export type { ActionError };
9
+
10
+ // ============================================================================
11
+ // Error Codes
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Hetzner API error codes
16
+ * @see https://docs.hetzner.cloud/#errors
17
+ */
18
+ export enum HetznerErrorCode {
19
+ // Authentication errors
20
+ Unauthorized = "unauthorized",
21
+ InvalidInput = "invalid_input",
22
+ JSONError = "json_error",
23
+ Forbidden = "forbidden",
24
+
25
+ // Resource errors
26
+ NotFound = "not_found",
27
+ ResourceLocked = "locked",
28
+ ResourceLimitExceeded = "resource_limit_exceeded",
29
+ UniquenessError = "uniqueness_error",
30
+
31
+ // Rate limiting
32
+ RateLimitExceeded = "rate_limit_exceeded",
33
+
34
+ // Conflict errors
35
+ Conflict = "conflict",
36
+ ServiceError = "service_error",
37
+
38
+ // Server-specific errors
39
+ ServerNotStopped = "server_not_stopped",
40
+ ServerAlreadyStopped = "server_already_stopped",
41
+ InvalidServerType = "invalid_server_type",
42
+
43
+ // IP/network errors
44
+ IpNotOwned = "ip_not_owned",
45
+ IpAlreadyAssigned = "ip_already_assigned",
46
+
47
+ // Volume errors
48
+ VolumeAlreadyAttached = "volume_already_attached",
49
+ VolumeSizeNotMultiple = "volume_size_not_multiple",
50
+
51
+ // Firewall errors
52
+ FirewallInUse = "firewall_in_use",
53
+
54
+ // Certificate errors
55
+ CertificateValidationFailed = "certificate_validation_failed",
56
+ CertificatePending = "certificate_pending",
57
+ }
58
+
59
+ // ============================================================================
60
+ // Error Classes
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Base Hetzner API error
65
+ */
66
+ export class HetznerAPIError extends Error {
67
+ constructor(
68
+ message: string,
69
+ public code?: string,
70
+ public details?: unknown
71
+ ) {
72
+ super(message);
73
+ this.name = "HetznerAPIError";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Authentication error (401)
79
+ */
80
+ export class HetznerUnauthorizedError extends HetznerAPIError {
81
+ constructor(message: string = "Unauthorized: Invalid API token") {
82
+ super(message, HetznerErrorCode.Unauthorized);
83
+ this.name = "HetznerUnauthorizedError";
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Forbidden error (403)
89
+ */
90
+ export class HetznerForbiddenError extends HetznerAPIError {
91
+ constructor(message: string = "Forbidden: Insufficient permissions") {
92
+ super(message, HetznerErrorCode.Forbidden);
93
+ this.name = "HetznerForbiddenError";
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Resource not found error (404)
99
+ */
100
+ export class HetznerNotFoundError extends HetznerAPIError {
101
+ constructor(resource: string, id: number | string) {
102
+ super(
103
+ `${resource} with ID ${id} not found`,
104
+ HetznerErrorCode.NotFound,
105
+ { resource, id }
106
+ );
107
+ this.name = "HetznerNotFoundError";
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Rate limit exceeded error (429)
113
+ */
114
+ export class HetznerRateLimitError extends HetznerAPIError {
115
+ constructor(
116
+ message: string = "Rate limit exceeded",
117
+ public rateLimitInfo?: RateLimitInfo
118
+ ) {
119
+ super(message, HetznerErrorCode.RateLimitExceeded, rateLimitInfo);
120
+ this.name = "HetznerRateLimitError";
121
+ }
122
+
123
+ /**
124
+ * Get the number of milliseconds until the rate limit resets
125
+ */
126
+ get resetInMs(): number {
127
+ if (!this.rateLimitInfo) return 60000; // Default to 1 minute
128
+ return Math.max(0, this.rateLimitInfo.reset * 1000 - Date.now());
129
+ }
130
+
131
+ /**
132
+ * Get a human-readable reset time
133
+ */
134
+ get resetTime(): string {
135
+ if (!this.rateLimitInfo) return "unknown";
136
+ return new Date(this.rateLimitInfo.reset * 1000).toISOString();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Resource locked error
142
+ */
143
+ export class HetznerResourceLockedError extends HetznerAPIError {
144
+ constructor(
145
+ resource: string,
146
+ id: number | string,
147
+ public actionInProgress?: string
148
+ ) {
149
+ super(
150
+ `${resource} ${id} is locked${actionInProgress ? ` by ${actionInProgress}` : ""}`,
151
+ HetznerErrorCode.ResourceLocked,
152
+ { resource, id, actionInProgress }
153
+ );
154
+ this.name = "HetznerResourceLockedError";
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Resource limit exceeded error
160
+ */
161
+ export class HetznerResourceLimitError extends HetznerAPIError {
162
+ constructor(resource: string, limit: number) {
163
+ super(
164
+ `Resource limit exceeded: ${resource} (limit: ${limit})`,
165
+ HetznerErrorCode.ResourceLimitExceeded,
166
+ { resource, limit }
167
+ );
168
+ this.name = "HetznerResourceLimitError";
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Invalid input error
174
+ */
175
+ export class HetznerInvalidInputError extends HetznerAPIError {
176
+ constructor(
177
+ message: string,
178
+ public fields?: Record<string, string>
179
+ ) {
180
+ super(message, HetznerErrorCode.InvalidInput, { fields });
181
+ this.name = "HetznerInvalidInputError";
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Conflict error
187
+ */
188
+ export class HetznerConflictError extends HetznerAPIError {
189
+ constructor(message: string, details?: unknown) {
190
+ super(message, HetznerErrorCode.Conflict, details);
191
+ this.name = "HetznerConflictError";
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Service error (5xx)
197
+ */
198
+ export class HetznerServiceError extends HetznerAPIError {
199
+ constructor(message: string, public statusCode?: number) {
200
+ super(message, HetznerErrorCode.ServiceError, { statusCode });
201
+ this.name = "HetznerServiceError";
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Action failed error
207
+ */
208
+ export class HetznerActionError extends HetznerAPIError {
209
+ constructor(
210
+ public actionError: ActionError,
211
+ public actionId: number
212
+ ) {
213
+ super(
214
+ `Action ${actionId} failed: ${actionError.code} - ${actionError.message}`,
215
+ actionError.code,
216
+ { actionError, actionId }
217
+ );
218
+ this.name = "HetznerActionError";
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Timeout error for action polling
224
+ */
225
+ export class HetznerTimeoutError extends HetznerAPIError {
226
+ constructor(
227
+ actionId: number,
228
+ timeout: number,
229
+ public lastProgress: number
230
+ ) {
231
+ super(
232
+ `Action ${actionId} timed out after ${timeout}ms (last progress: ${lastProgress}%)`,
233
+ "timeout",
234
+ { actionId, timeout, lastProgress }
235
+ );
236
+ this.name = "HetznerTimeoutError";
237
+ }
238
+ }
239
+
240
+ // ============================================================================
241
+ // Error Factory
242
+ // ============================================================================
243
+
244
+ /**
245
+ * Parse Hetzner API error response and create appropriate error
246
+ */
247
+ export function createHetznerError(
248
+ statusCode: number,
249
+ body: {
250
+ error?: {
251
+ code: string;
252
+ message: string;
253
+ details?: unknown;
254
+ };
255
+ }
256
+ ): HetznerAPIError {
257
+ const error = body.error;
258
+
259
+ if (!error) {
260
+ return new HetznerServiceError(
261
+ `HTTP ${statusCode}: ${JSON.stringify(body)}`,
262
+ statusCode
263
+ );
264
+ }
265
+
266
+ switch (statusCode) {
267
+ case 401:
268
+ return new HetznerUnauthorizedError(error.message);
269
+ case 403:
270
+ return new HetznerForbiddenError(error.message);
271
+ case 404:
272
+ return new HetznerNotFoundError("resource", "unknown");
273
+ case 429:
274
+ return new HetznerRateLimitError(error.message);
275
+ case 400:
276
+ if (error.code === HetznerErrorCode.ResourceLocked) {
277
+ return new HetznerResourceLockedError(
278
+ "resource",
279
+ "unknown",
280
+ error.message
281
+ );
282
+ }
283
+ if (error.code === HetznerErrorCode.InvalidInput) {
284
+ return new HetznerInvalidInputError(error.message);
285
+ }
286
+ return new HetznerInvalidInputError(error.message);
287
+ case 409:
288
+ return new HetznerConflictError(error.message, error.details);
289
+ default:
290
+ if (statusCode >= 500) {
291
+ return new HetznerServiceError(error.message, statusCode);
292
+ }
293
+ return new HetznerAPIError(error.message, error.code, error.details);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Check if an error is retryable
299
+ */
300
+ export function isRetryableError(error: unknown): boolean {
301
+ if (error instanceof HetznerRateLimitError) return true;
302
+ if (error instanceof HetznerResourceLockedError) return true;
303
+ if (error instanceof HetznerServiceError) return true;
304
+ if (error instanceof HetznerConflictError) return true;
305
+ return false;
306
+ }
307
+
308
+ /**
309
+ * Check if an error is a rate limit error
310
+ */
311
+ export function isRateLimitError(error: unknown): error is HetznerRateLimitError {
312
+ return error instanceof HetznerRateLimitError;
313
+ }
314
+
315
+ /**
316
+ * Check if an error is a resource locked error
317
+ */
318
+ export function isResourceLockedError(
319
+ error: unknown
320
+ ): error is HetznerResourceLockedError {
321
+ return error instanceof HetznerResourceLockedError;
322
+ }
323
+
324
+ /**
325
+ * Calculate retry delay with exponential backoff
326
+ */
327
+ export function calculateRetryDelay(
328
+ attempt: number,
329
+ baseDelay: number = 1000,
330
+ maxDelay: number = 60000
331
+ ): number {
332
+ const delay = baseDelay * Math.pow(2, attempt);
333
+ // Add jitter (±25%)
334
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
335
+ return Math.min(maxDelay, delay + jitter);
336
+ }
337
+
338
+ // ============================================================================
339
+ // Error Handler Types
340
+ // ============================================================================
341
+
342
+ /**
343
+ * Error handler function type
344
+ */
345
+ export type ErrorHandler = (error: HetznerAPIError) => void | Promise<void>;
346
+
347
+ /**
348
+ * Error handler options
349
+ */
350
+ export interface ErrorHandlerContext {
351
+ /** Maximum number of retry attempts */
352
+ maxRetries?: number;
353
+ /** Base delay for exponential backoff in milliseconds */
354
+ baseDelay?: number;
355
+ /** Maximum delay between retries in milliseconds */
356
+ maxDelay?: number;
357
+ /** Optional error handler callback */
358
+ onError?: ErrorHandler;
359
+ /** Whether to log errors */
360
+ logErrors?: boolean;
361
+ }
362
+
363
+ /**
364
+ * Default error handler that logs to console
365
+ */
366
+ export function defaultErrorHandler(error: HetznerAPIError): void {
367
+ console.error(`[Hetzner API Error] ${error.name}: ${error.message}`);
368
+ if (error.details) {
369
+ console.error("Details:", error.details);
370
+ }
371
+ }
package/index.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Hetzner Cloud API client
3
+ * For server-side use only (requires API token)
4
+ *
5
+ * TODO:
6
+ * - Certificate actions: https://docs.hetzner.cloud/reference/cloud#certificate-actions
7
+ * - DNS operations
8
+ */
9
+ // Core exports
10
+ export { HetznerClient } from "./client.js";
11
+ export { ServerOperations } from "./servers.js";
12
+ export { ActionOperations } from "./actions.js";
13
+ export { SSHKeyOperations } from "./ssh-keys.js";
14
+ // Auth
15
+ export { getTokenFromCLI, isAuthenticated, resolveApiToken } from "./auth.js";
16
+ // Config
17
+ export { HETZNER_API_BASE } from "./config.js";
18
+ // Types
19
+ export * from "./types.js";
20
+ // Schemas
21
+ export * from "./schemas.js";
22
+ // Errors
23
+ export * from "./errors.js";
24
+ // Action utilities
25
+ export { waitForAction, waitForMultipleActions, waitForMultipleActionsWithLimit, batchCheckActions, getActionTimeout, isActionRunning, isActionSuccess, isActionError, formatActionProgress, getActionDescription, getPollInterval, getAdaptivePollInterval, waitForActionAdaptive, parseRateLimitHeaders, isRateLimitLow, formatRateLimitStatus, waitForRateLimitReset, createProgressLogger, ACTION_TIMEOUTS, } from "./actions.js";
26
+ // Bootstrap security modules
27
+ export * from "./bootstrap/index.js";
28
+ //# sourceMappingURL=index.js.map