@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.
- package/actions.js +802 -0
- package/actions.ts +1053 -0
- package/auth.js +35 -0
- package/auth.ts +37 -0
- package/bootstrap/FIREWALL.md +326 -0
- package/bootstrap/KERNEL-HARDENING.md +258 -0
- package/bootstrap/SECURITY-INTEGRATION.md +281 -0
- package/bootstrap/TESTING.md +301 -0
- package/bootstrap/cloud-init.js +279 -0
- package/bootstrap/cloud-init.ts +394 -0
- package/bootstrap/firewall.js +279 -0
- package/bootstrap/firewall.ts +342 -0
- package/bootstrap/genesis.js +406 -0
- package/bootstrap/genesis.ts +518 -0
- package/bootstrap/index.js +35 -0
- package/bootstrap/index.ts +71 -0
- package/bootstrap/kernel-hardening.js +266 -0
- package/bootstrap/kernel-hardening.test.ts +230 -0
- package/bootstrap/kernel-hardening.ts +272 -0
- package/bootstrap/security-audit.js +118 -0
- package/bootstrap/security-audit.ts +124 -0
- package/bootstrap/ssh-hardening.js +182 -0
- package/bootstrap/ssh-hardening.ts +192 -0
- package/client.js +137 -0
- package/client.ts +177 -0
- package/config.js +5 -0
- package/config.ts +5 -0
- package/errors.js +270 -0
- package/errors.ts +371 -0
- package/index.js +28 -0
- package/index.ts +55 -0
- package/package.json +56 -0
- package/pricing.js +284 -0
- package/pricing.ts +422 -0
- package/schemas.js +660 -0
- package/schemas.ts +765 -0
- package/server-status.ts +81 -0
- package/servers.js +424 -0
- package/servers.ts +568 -0
- package/ssh-keys.js +90 -0
- package/ssh-keys.ts +122 -0
- package/ssh-setup.ts +218 -0
- package/types.js +96 -0
- package/types.ts +389 -0
- package/volumes.js +172 -0
- package/volumes.ts +229 -0
package/config.js
ADDED
package/config.ts
ADDED
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
|