@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/schemas.ts
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hetzner Cloud API Zod validation schemas
|
|
3
|
+
*
|
|
4
|
+
* These schemas provide runtime validation for API responses
|
|
5
|
+
* and help ensure type safety throughout the application.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// Import status enums to use in schema validation
|
|
11
|
+
import {
|
|
12
|
+
EnvironmentStatus,
|
|
13
|
+
ActionStatus,
|
|
14
|
+
VolumeStatus,
|
|
15
|
+
} from "@ebowwa/codespaces-types/compile";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Helper Validators
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* IPv4 address validator
|
|
23
|
+
*/
|
|
24
|
+
const ipv4Regex =
|
|
25
|
+
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* IPv6 address validator
|
|
29
|
+
* Hetzner returns IPv6 in CIDR notation which can vary in format
|
|
30
|
+
* Using a more lenient pattern since exact format varies
|
|
31
|
+
*/
|
|
32
|
+
const ipv6Regex = /^[0-9a-fA-F:]+(?:\/\d{1,3})?$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* IP address validator (IPv4 or IPv6)
|
|
36
|
+
*/
|
|
37
|
+
const ipRegex =
|
|
38
|
+
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Base Schemas
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Hetzner API error response schema
|
|
46
|
+
*/
|
|
47
|
+
export const HetznerErrorSchema = z.object({
|
|
48
|
+
code: z.string(),
|
|
49
|
+
message: z.string(),
|
|
50
|
+
details: z.any().optional(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pagination metadata schema
|
|
55
|
+
*/
|
|
56
|
+
export const HetznerPaginationSchema = z.object({
|
|
57
|
+
page: z.number(),
|
|
58
|
+
per_page: z.number(),
|
|
59
|
+
previous_page: z.number().nullable(),
|
|
60
|
+
next_page: z.number().nullable(),
|
|
61
|
+
last_page: z.number(),
|
|
62
|
+
total_entries: z.number(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Metadata wrapper schema
|
|
67
|
+
*/
|
|
68
|
+
export const HetznerMetaSchema = z.object({
|
|
69
|
+
pagination: HetznerPaginationSchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Action Schemas
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Action resource schema
|
|
78
|
+
*/
|
|
79
|
+
export const HetznerActionResourceSchema = z.object({
|
|
80
|
+
id: z.number(),
|
|
81
|
+
type: z.enum([
|
|
82
|
+
"server",
|
|
83
|
+
"volume",
|
|
84
|
+
"network",
|
|
85
|
+
"floating_ip",
|
|
86
|
+
"load_balancer",
|
|
87
|
+
"certificate",
|
|
88
|
+
"firewall",
|
|
89
|
+
"image",
|
|
90
|
+
]),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Action error schema
|
|
95
|
+
*/
|
|
96
|
+
export const HetznerActionErrorSchema = z.object({
|
|
97
|
+
code: z.string(),
|
|
98
|
+
message: z.string(),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Base action schema
|
|
103
|
+
*/
|
|
104
|
+
export const HetznerActionSchema = z.object({
|
|
105
|
+
id: z.number(),
|
|
106
|
+
command: z.string(), // API returns string, not ActionCommand enum
|
|
107
|
+
status: z.nativeEnum(ActionStatus),
|
|
108
|
+
started: z.string(),
|
|
109
|
+
finished: z.string().nullable(),
|
|
110
|
+
progress: z.number().min(0).max(100),
|
|
111
|
+
resources: z.array(HetznerActionResourceSchema),
|
|
112
|
+
error: HetznerActionErrorSchema.nullable(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Action response schema
|
|
117
|
+
*/
|
|
118
|
+
export const HetznerActionResponseSchema = z.object({
|
|
119
|
+
action: HetznerActionSchema,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Actions list response schema
|
|
124
|
+
*/
|
|
125
|
+
export const HetznerActionsResponseSchema = z.object({
|
|
126
|
+
actions: z.array(HetznerActionSchema),
|
|
127
|
+
meta: HetznerMetaSchema,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Server Schemas
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Server image schema
|
|
136
|
+
*/
|
|
137
|
+
export const HetznerServerImageSchema = z.object({
|
|
138
|
+
id: z.number(),
|
|
139
|
+
name: z.string(),
|
|
140
|
+
description: z.string(),
|
|
141
|
+
type: z.enum(["snapshot", "backup", "system"]),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Server IPv4 schema
|
|
146
|
+
*/
|
|
147
|
+
export const HetznerIPv4Schema = z.object({
|
|
148
|
+
ip: z.string().regex(ipv4Regex),
|
|
149
|
+
blocked: z.boolean(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Server IPv6 schema
|
|
154
|
+
*/
|
|
155
|
+
export const HetznerIPv6Schema = z.object({
|
|
156
|
+
ip: z.string().regex(ipv6Regex),
|
|
157
|
+
blocked: z.boolean(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Server floating IP reference schema
|
|
162
|
+
*/
|
|
163
|
+
export const HetznerFloatingIpRefSchema = z.object({
|
|
164
|
+
id: z.number(),
|
|
165
|
+
ip: z.string(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Server firewall reference schema
|
|
170
|
+
*/
|
|
171
|
+
export const HetznerFirewallRefSchema = z.object({
|
|
172
|
+
id: z.number(),
|
|
173
|
+
name: z.string(),
|
|
174
|
+
status: z.enum(["applied", "pending"]),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Server public network schema
|
|
179
|
+
*/
|
|
180
|
+
export const HetznerPublicNetSchema = z.object({
|
|
181
|
+
ipv4: HetznerIPv4Schema,
|
|
182
|
+
ipv6: HetznerIPv6Schema.optional(),
|
|
183
|
+
floating_ips: z.array(HetznerFloatingIpRefSchema),
|
|
184
|
+
firewalls: z.array(HetznerFirewallRefSchema),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Server type schema
|
|
189
|
+
*/
|
|
190
|
+
export const HetznerServerTypeSchema = z.object({
|
|
191
|
+
id: z.number(),
|
|
192
|
+
name: z.string(),
|
|
193
|
+
description: z.string(),
|
|
194
|
+
cores: z.number(),
|
|
195
|
+
memory: z.number(),
|
|
196
|
+
disk: z.number(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Location schema
|
|
201
|
+
*/
|
|
202
|
+
export const HetznerLocationSchema = z.object({
|
|
203
|
+
id: z.number(),
|
|
204
|
+
name: z.string(),
|
|
205
|
+
description: z.string(),
|
|
206
|
+
country: z.string(),
|
|
207
|
+
city: z.string(),
|
|
208
|
+
latitude: z.number(),
|
|
209
|
+
longitude: z.number(),
|
|
210
|
+
network_zone: z.string(),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Datacenter schema
|
|
215
|
+
*/
|
|
216
|
+
export const HetznerDatacenterSchema = z.object({
|
|
217
|
+
id: z.number(),
|
|
218
|
+
name: z.string(),
|
|
219
|
+
description: z.string(),
|
|
220
|
+
location: HetznerLocationSchema,
|
|
221
|
+
// Hetzner API sometimes doesn't return this field
|
|
222
|
+
supported_server_types: z
|
|
223
|
+
.array(
|
|
224
|
+
z.object({
|
|
225
|
+
id: z.number(),
|
|
226
|
+
name: z.string(),
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
.optional()
|
|
230
|
+
.nullable(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Server volume reference schema
|
|
235
|
+
*/
|
|
236
|
+
export const HetznerVolumeRefSchema = z.object({
|
|
237
|
+
id: z.number(),
|
|
238
|
+
name: z.string(),
|
|
239
|
+
size: z.number().positive(),
|
|
240
|
+
linux_device: z.string(),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Server protection schema
|
|
245
|
+
*/
|
|
246
|
+
export const HetznerServerProtectionSchema = z.object({
|
|
247
|
+
delete: z.boolean(),
|
|
248
|
+
rebuild: z.boolean(),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Full server schema
|
|
253
|
+
*/
|
|
254
|
+
export const HetznerServerSchema = z.object({
|
|
255
|
+
id: z.number().positive(),
|
|
256
|
+
name: z.string().min(1),
|
|
257
|
+
status: z.nativeEnum(EnvironmentStatus),
|
|
258
|
+
image: HetznerServerImageSchema.nullable().optional(),
|
|
259
|
+
public_net: HetznerPublicNetSchema,
|
|
260
|
+
server_type: HetznerServerTypeSchema,
|
|
261
|
+
datacenter: HetznerDatacenterSchema,
|
|
262
|
+
labels: z.record(z.string(), z.any()),
|
|
263
|
+
created: z.string().datetime(),
|
|
264
|
+
protection: HetznerServerProtectionSchema,
|
|
265
|
+
volumes: z.array(HetznerVolumeRefSchema),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* List servers response schema
|
|
270
|
+
*/
|
|
271
|
+
export const HetznerListServersResponseSchema = z.object({
|
|
272
|
+
servers: z.array(HetznerServerSchema),
|
|
273
|
+
meta: HetznerMetaSchema,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get server response schema
|
|
278
|
+
*/
|
|
279
|
+
export const HetznerGetServerResponseSchema = z.object({
|
|
280
|
+
server: HetznerServerSchema,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create server request options schema
|
|
285
|
+
*/
|
|
286
|
+
export const HetznerCreateServerRequestSchema = z
|
|
287
|
+
.object({
|
|
288
|
+
name: z
|
|
289
|
+
.string()
|
|
290
|
+
.min(1)
|
|
291
|
+
.max(64)
|
|
292
|
+
.regex(
|
|
293
|
+
/^[a-zA-Z0-9][a-zA-Z0-9-]*$/,
|
|
294
|
+
"Name must start with letter/number and contain only letters, numbers, and hyphens",
|
|
295
|
+
),
|
|
296
|
+
server_type: z.string().min(1),
|
|
297
|
+
image: z.string().min(1),
|
|
298
|
+
location: z.string().min(1).optional(),
|
|
299
|
+
datacenter: z.string().min(1).optional(),
|
|
300
|
+
ssh_keys: z.array(z.union([z.string(), z.number()])).optional(),
|
|
301
|
+
volumes: z.array(z.number().positive()).optional(),
|
|
302
|
+
labels: z.record(z.string(), z.any()).optional(),
|
|
303
|
+
start_after_create: z.boolean().optional(),
|
|
304
|
+
})
|
|
305
|
+
.refine(
|
|
306
|
+
(data) => !(data.location && data.datacenter),
|
|
307
|
+
"Cannot specify both location and datacenter",
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Create server response schema
|
|
312
|
+
*/
|
|
313
|
+
export const HetznerCreateServerResponseSchema = z.object({
|
|
314
|
+
server: HetznerServerSchema,
|
|
315
|
+
action: HetznerActionSchema,
|
|
316
|
+
next_actions: z.array(HetznerActionSchema),
|
|
317
|
+
root_password: z.string().nullable(),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Update server request schema
|
|
322
|
+
*/
|
|
323
|
+
export const HetznerUpdateServerRequestSchema = z.object({
|
|
324
|
+
name: z.string().min(1).max(64).optional(),
|
|
325
|
+
labels: z.record(z.string(), z.any()).optional(),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Update server response schema
|
|
330
|
+
*/
|
|
331
|
+
export const HetznerUpdateServerResponseSchema = z.object({
|
|
332
|
+
server: HetznerServerSchema,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ============================================================================
|
|
336
|
+
// Volume Schemas
|
|
337
|
+
// ============================================================================
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Volume location schema
|
|
341
|
+
*/
|
|
342
|
+
export const HetznerVolumeLocationSchema = z.object({
|
|
343
|
+
id: z.number(),
|
|
344
|
+
name: z.string(),
|
|
345
|
+
description: z.string(),
|
|
346
|
+
country: z.string(),
|
|
347
|
+
city: z.string(),
|
|
348
|
+
latitude: z.number(),
|
|
349
|
+
longitude: z.number(),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Volume protection schema
|
|
354
|
+
*/
|
|
355
|
+
export const HetznerVolumeProtectionSchema = z.object({
|
|
356
|
+
delete: z.boolean(),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Volume schema
|
|
361
|
+
*/
|
|
362
|
+
export const HetznerVolumeSchema = z.object({
|
|
363
|
+
id: z.number().positive(),
|
|
364
|
+
name: z.string().min(1),
|
|
365
|
+
status: z.nativeEnum(VolumeStatus),
|
|
366
|
+
server: z.number().positive().nullable(),
|
|
367
|
+
size: z.number().positive(),
|
|
368
|
+
linux_device: z.string().nullable(),
|
|
369
|
+
format: z.string().nullable(),
|
|
370
|
+
location: HetznerVolumeLocationSchema.nullable(),
|
|
371
|
+
labels: z.record(z.string(), z.any()),
|
|
372
|
+
created: z.string().datetime(),
|
|
373
|
+
protection: HetznerVolumeProtectionSchema,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* List volumes response schema
|
|
378
|
+
*/
|
|
379
|
+
export const HetznerListVolumesResponseSchema = z.object({
|
|
380
|
+
volumes: z.array(HetznerVolumeSchema),
|
|
381
|
+
meta: HetznerMetaSchema,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Get volume response schema
|
|
386
|
+
*/
|
|
387
|
+
export const HetznerGetVolumeResponseSchema = z.object({
|
|
388
|
+
volume: HetznerVolumeSchema,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Create volume request schema
|
|
393
|
+
*/
|
|
394
|
+
export const HetznerCreateVolumeRequestSchema = z
|
|
395
|
+
.object({
|
|
396
|
+
name: z.string().min(1).max(64),
|
|
397
|
+
size: z.number().positive().multipleOf(1), // GB
|
|
398
|
+
server: z.number().positive().optional(),
|
|
399
|
+
location: z.string().min(1).optional(),
|
|
400
|
+
automount: z.boolean().optional(),
|
|
401
|
+
format: z.string().optional(),
|
|
402
|
+
labels: z.record(z.string(), z.any()).optional(),
|
|
403
|
+
})
|
|
404
|
+
.refine((data) => {
|
|
405
|
+
// Ensure size is a multiple of GB (1, 2, 3, etc.)
|
|
406
|
+
return Number.isInteger(data.size);
|
|
407
|
+
}, "Volume size must be a whole number in GB");
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create volume response schema
|
|
411
|
+
*/
|
|
412
|
+
export const HetznerCreateVolumeResponseSchema = z.object({
|
|
413
|
+
volume: HetznerVolumeSchema,
|
|
414
|
+
action: HetznerActionSchema,
|
|
415
|
+
next_actions: z.array(HetznerActionSchema),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Network Schemas
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Network subnet schema
|
|
424
|
+
*/
|
|
425
|
+
export const HetznerSubnetSchema = z.object({
|
|
426
|
+
type: z.enum(["server", "cloud", "vswitch"]),
|
|
427
|
+
ip_range: z.string().regex(ipRegex),
|
|
428
|
+
network_zone: z.string(),
|
|
429
|
+
gateway: z.string().regex(ipRegex),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Network route schema
|
|
434
|
+
*/
|
|
435
|
+
export const HetznerRouteSchema = z.object({
|
|
436
|
+
destination: z.string().regex(ipRegex),
|
|
437
|
+
gateway: z.string().regex(ipRegex),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Network protection schema
|
|
442
|
+
*/
|
|
443
|
+
export const HetznerNetworkProtectionSchema = z.object({
|
|
444
|
+
delete: z.boolean(),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Network schema
|
|
449
|
+
*/
|
|
450
|
+
export const HetznerNetworkSchema = z.object({
|
|
451
|
+
id: z.number().positive(),
|
|
452
|
+
name: z.string().min(1).max(64),
|
|
453
|
+
ip_range: z.string().regex(ipRegex),
|
|
454
|
+
subnets: z.array(HetznerSubnetSchema),
|
|
455
|
+
routes: z.array(HetznerRouteSchema),
|
|
456
|
+
servers: z.array(z.number().positive()),
|
|
457
|
+
protection: HetznerNetworkProtectionSchema,
|
|
458
|
+
labels: z.record(z.string(), z.any()),
|
|
459
|
+
created: z.string().datetime(),
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* List networks response schema
|
|
464
|
+
*/
|
|
465
|
+
export const HetznerListNetworksResponseSchema = z.object({
|
|
466
|
+
networks: z.array(HetznerNetworkSchema),
|
|
467
|
+
meta: HetznerMetaSchema,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Get network response schema
|
|
472
|
+
*/
|
|
473
|
+
export const HetznerGetNetworkResponseSchema = z.object({
|
|
474
|
+
network: HetznerNetworkSchema,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ============================================================================
|
|
478
|
+
// SSH Key Schemas
|
|
479
|
+
// ============================================================================
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* SSH key schema
|
|
483
|
+
*/
|
|
484
|
+
export const HetznerSSHKeySchema = z.object({
|
|
485
|
+
id: z.number().positive(),
|
|
486
|
+
name: z.string().min(1).max(64),
|
|
487
|
+
fingerprint: z.string(),
|
|
488
|
+
public_key: z.string(),
|
|
489
|
+
labels: z.record(z.string(), z.any()),
|
|
490
|
+
created: z.string().datetime(),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* List SSH keys response schema
|
|
495
|
+
*/
|
|
496
|
+
export const HetznerListSSHKeysResponseSchema = z.object({
|
|
497
|
+
ssh_keys: z.array(HetznerSSHKeySchema),
|
|
498
|
+
meta: HetznerMetaSchema,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Get SSH key response schema
|
|
503
|
+
*/
|
|
504
|
+
export const HetznerGetSSHKeyResponseSchema = z.object({
|
|
505
|
+
ssh_key: HetznerSSHKeySchema,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Create SSH key request schema
|
|
510
|
+
*/
|
|
511
|
+
export const HetznerCreateSSHKeyRequestSchema = z.object({
|
|
512
|
+
name: z
|
|
513
|
+
.string()
|
|
514
|
+
.min(1)
|
|
515
|
+
.max(64)
|
|
516
|
+
.regex(
|
|
517
|
+
/^[a-zA-Z0-9][a-zA-Z0-9-]*$/,
|
|
518
|
+
"Name must start with letter/number and contain only letters, numbers, and hyphens",
|
|
519
|
+
),
|
|
520
|
+
public_key: z.string().min(1),
|
|
521
|
+
labels: z.record(z.string(), z.any()).optional(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Create SSH key response schema
|
|
526
|
+
*/
|
|
527
|
+
export const HetznerCreateSSHKeyResponseSchema = z.object({
|
|
528
|
+
ssh_key: HetznerSSHKeySchema,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// Floating IP Schemas
|
|
533
|
+
// ============================================================================
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Floating IP schema
|
|
537
|
+
*/
|
|
538
|
+
export const HetznerFloatingIpSchema = z.object({
|
|
539
|
+
id: z.number().positive(),
|
|
540
|
+
name: z.string().min(1).max(64),
|
|
541
|
+
description: z.string().optional(),
|
|
542
|
+
type: z.enum(["ipv4", "ipv6"]),
|
|
543
|
+
ip: z.string().regex(ipRegex),
|
|
544
|
+
server: z.number().positive().nullable(),
|
|
545
|
+
dns_ptr: z.array(
|
|
546
|
+
z.object({
|
|
547
|
+
ip: z.string(),
|
|
548
|
+
dns_ptr: z.string(),
|
|
549
|
+
}),
|
|
550
|
+
),
|
|
551
|
+
home_location: HetznerLocationSchema,
|
|
552
|
+
blocked: z.boolean(),
|
|
553
|
+
protection: z.object({
|
|
554
|
+
delete: z.boolean(),
|
|
555
|
+
}),
|
|
556
|
+
labels: z.record(z.string(), z.any()),
|
|
557
|
+
created: z.string().datetime(),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* List floating IPs response schema
|
|
562
|
+
*/
|
|
563
|
+
export const HetznerListFloatingIpsResponseSchema = z.object({
|
|
564
|
+
floating_ips: z.array(HetznerFloatingIpSchema),
|
|
565
|
+
meta: HetznerMetaSchema,
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
// ============================================================================
|
|
569
|
+
// Firewall Schemas
|
|
570
|
+
// ============================================================================
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Firewall rule schema
|
|
574
|
+
*/
|
|
575
|
+
export const HetznerFirewallRuleSchema = z.object({
|
|
576
|
+
direction: z.enum(["in", "out"]),
|
|
577
|
+
source_ips: z.array(z.string().regex(ipRegex)).optional(),
|
|
578
|
+
destination_ips: z.array(z.string().regex(ipRegex)).optional(),
|
|
579
|
+
source_port: z.string().optional(),
|
|
580
|
+
destination_port: z.string().optional(),
|
|
581
|
+
protocol: z.enum(["tcp", "udp", "icmp", "esp", "gre"]),
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Firewall resource schema
|
|
586
|
+
*/
|
|
587
|
+
export const HetznerFirewallResourceSchema = z.object({
|
|
588
|
+
type: z.enum(["server", "label_selector"]),
|
|
589
|
+
server: z
|
|
590
|
+
.object({
|
|
591
|
+
id: z.number().positive(),
|
|
592
|
+
})
|
|
593
|
+
.optional(),
|
|
594
|
+
label_selector: z
|
|
595
|
+
.object({
|
|
596
|
+
selector: z.string(),
|
|
597
|
+
})
|
|
598
|
+
.optional(),
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Firewall schema
|
|
603
|
+
*/
|
|
604
|
+
export const HetznerFirewallSchema = z.object({
|
|
605
|
+
id: z.number().positive(),
|
|
606
|
+
name: z.string().min(1).max(64),
|
|
607
|
+
rules: z.array(HetznerFirewallRuleSchema),
|
|
608
|
+
apply_to: z.array(HetznerFirewallResourceSchema),
|
|
609
|
+
labels: z.record(z.string(), z.any()),
|
|
610
|
+
created: z.string().datetime(),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* List firewalls response schema
|
|
615
|
+
*/
|
|
616
|
+
export const HetznerListFirewallsResponseSchema = z.object({
|
|
617
|
+
firewalls: z.array(HetznerFirewallSchema),
|
|
618
|
+
meta: HetznerMetaSchema,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ============================================================================
|
|
622
|
+
// ISO Schemas
|
|
623
|
+
// ============================================================================
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* ISO schema
|
|
627
|
+
*/
|
|
628
|
+
export const HetznerIsoSchema = z.object({
|
|
629
|
+
id: z.number().positive(),
|
|
630
|
+
name: z.string(),
|
|
631
|
+
description: z.string(),
|
|
632
|
+
type: z.enum(["public", "private"]),
|
|
633
|
+
deprecated: z.date().nullable().optional(),
|
|
634
|
+
architecture: z.array(z.enum(["x86", "arm"])).optional(),
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* List ISOs response schema
|
|
639
|
+
*/
|
|
640
|
+
export const HetznerListIsosResponseSchema = z.object({
|
|
641
|
+
isos: z.array(HetznerIsoSchema),
|
|
642
|
+
meta: HetznerMetaSchema,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ============================================================================
|
|
646
|
+
// Location Schemas
|
|
647
|
+
// ============================================================================
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* List locations response schema
|
|
651
|
+
*/
|
|
652
|
+
export const HetznerListLocationsResponseSchema = z.object({
|
|
653
|
+
locations: z.array(HetznerLocationSchema),
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// ============================================================================
|
|
657
|
+
// Datacenter Schemas
|
|
658
|
+
// ============================================================================
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* List datacenters response schema
|
|
662
|
+
*/
|
|
663
|
+
export const HetznerListDatacentersResponseSchema = z.object({
|
|
664
|
+
datacenters: z.array(HetznerDatacenterSchema),
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// Server Type Schemas
|
|
669
|
+
// ============================================================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Server type pricing schema
|
|
673
|
+
*
|
|
674
|
+
* Note: location can be null in some Hetzner API responses
|
|
675
|
+
*/
|
|
676
|
+
export const HetznerServerTypePricingSchema = z.object({
|
|
677
|
+
location: z.string().nullable(),
|
|
678
|
+
price_hourly: z.object({
|
|
679
|
+
net: z.string(),
|
|
680
|
+
gross: z.string(),
|
|
681
|
+
}),
|
|
682
|
+
price_monthly: z.object({
|
|
683
|
+
net: z.string(),
|
|
684
|
+
gross: z.string(),
|
|
685
|
+
}),
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Extended server type schema (for listing)
|
|
690
|
+
*/
|
|
691
|
+
export const HetznerServerTypeExtendedSchema = HetznerServerTypeSchema.extend({
|
|
692
|
+
deprecated: z.boolean().optional(),
|
|
693
|
+
prices: z.array(HetznerServerTypePricingSchema),
|
|
694
|
+
storage_type: z.enum(["local", "network"]),
|
|
695
|
+
cpu_type: z.enum(["shared", "dedicated"]),
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* List server types response schema
|
|
700
|
+
*/
|
|
701
|
+
export const HetznerListServerTypesResponseSchema = z.object({
|
|
702
|
+
server_types: z.array(HetznerServerTypeExtendedSchema),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// ============================================================================
|
|
706
|
+
// Certificate Schemas
|
|
707
|
+
// ============================================================================
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Certificate schema
|
|
711
|
+
*/
|
|
712
|
+
export const HetznerCertificateSchema = z.object({
|
|
713
|
+
id: z.number().positive(),
|
|
714
|
+
name: z.string().min(1).max(64),
|
|
715
|
+
labels: z.record(z.string(), z.any()),
|
|
716
|
+
certificate: z.string(),
|
|
717
|
+
not_valid_before: z.string().datetime(),
|
|
718
|
+
not_valid_after: z.string().datetime(),
|
|
719
|
+
domain_names: z.array(z.string().url()),
|
|
720
|
+
fingerprint: z.string(),
|
|
721
|
+
created: z.string().datetime(),
|
|
722
|
+
status: z.enum(["pending", "issued", "failed", "revoked"]),
|
|
723
|
+
failed: z.boolean().optional(),
|
|
724
|
+
type: z.enum(["uploaded", "managed"]),
|
|
725
|
+
usage: z
|
|
726
|
+
.array(z.enum(["dual_stack", "server", "load_balancer", "dns"]))
|
|
727
|
+
.optional(),
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* List certificates response schema
|
|
732
|
+
*/
|
|
733
|
+
export const HetznerListCertificatesResponseSchema = z.object({
|
|
734
|
+
certificates: z.array(HetznerCertificateSchema),
|
|
735
|
+
meta: HetznerMetaSchema,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// ============================================================================
|
|
739
|
+
// Generic Response Wrappers
|
|
740
|
+
// ============================================================================
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Generic paginated response schema
|
|
744
|
+
*/
|
|
745
|
+
export function createPaginatedResponseSchema<T extends z.ZodType>(
|
|
746
|
+
itemSchema: T,
|
|
747
|
+
itemName: string,
|
|
748
|
+
) {
|
|
749
|
+
return z.object({
|
|
750
|
+
[itemName]: z.array(itemSchema),
|
|
751
|
+
meta: HetznerMetaSchema,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Generic single item response schema
|
|
757
|
+
*/
|
|
758
|
+
export function createItemResponseSchema<T extends z.ZodType>(
|
|
759
|
+
itemSchema: T,
|
|
760
|
+
itemName: string,
|
|
761
|
+
) {
|
|
762
|
+
return z.object({
|
|
763
|
+
[itemName]: itemSchema,
|
|
764
|
+
});
|
|
765
|
+
}
|