@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/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
+ }