@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/index.ts ADDED
@@ -0,0 +1,55 @@
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
+
10
+ // Core exports
11
+ export { HetznerClient } from "./client.js";
12
+ export { ServerOperations } from "./servers.js";
13
+ export { ActionOperations } from "./actions.js";
14
+ export { SSHKeyOperations } from "./ssh-keys.js";
15
+
16
+ // Auth
17
+ export { getTokenFromCLI, isAuthenticated, resolveApiToken } from "./auth.js";
18
+
19
+ // Config
20
+ export { HETZNER_API_BASE } from "./config.js";
21
+
22
+ // Types
23
+ export * from "./types.js";
24
+
25
+ // Schemas
26
+ export * from "./schemas.js";
27
+
28
+ // Errors
29
+ export * from "./errors.js";
30
+
31
+ // Action utilities
32
+ export {
33
+ waitForAction,
34
+ waitForMultipleActions,
35
+ waitForMultipleActionsWithLimit,
36
+ batchCheckActions,
37
+ getActionTimeout,
38
+ isActionRunning,
39
+ isActionSuccess,
40
+ isActionError,
41
+ formatActionProgress,
42
+ getActionDescription,
43
+ getPollInterval,
44
+ getAdaptivePollInterval,
45
+ waitForActionAdaptive,
46
+ parseRateLimitHeaders,
47
+ isRateLimitLow,
48
+ formatRateLimitStatus,
49
+ waitForRateLimitReset,
50
+ createProgressLogger,
51
+ ACTION_TIMEOUTS,
52
+ } from "./actions.js";
53
+
54
+ // Bootstrap security modules
55
+ export * from "./bootstrap/index.js";
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@ebowwa/hetzner",
3
+ "version": "0.1.0",
4
+ "description": "Hetzner Cloud API client - servers, volumes, SSH keys, actions, pricing",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "types": "./index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.ts",
11
+ "default": "./index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./client.ts",
15
+ "default": "./client.js"
16
+ },
17
+ "./types": {
18
+ "types": "./types.ts",
19
+ "default": "./types.js"
20
+ },
21
+ "./schemas": {
22
+ "types": "./schemas.ts",
23
+ "default": "./schemas.js"
24
+ },
25
+ "./bootstrap": {
26
+ "types": "./bootstrap/index.ts",
27
+ "default": "./bootstrap/index.js"
28
+ }
29
+ },
30
+ "keywords": [
31
+ "hetzner",
32
+ "cloud",
33
+ "vps",
34
+ "api",
35
+ "servers",
36
+ "volumes",
37
+ "ssh"
38
+ ],
39
+ "author": "Ebowwa Labs <labs@ebowwa.com>",
40
+ "license": "MIT",
41
+ "homepage": "https://github.com/ebowwa/codespaces/tree/main/packages/src/hetzner#readme",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/ebowwa/codespaces.git",
45
+ "directory": "packages/src/hetzner"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/ebowwa/codespaces/issues"
49
+ },
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ },
53
+ "dependencies": {
54
+ "zod": "^4.3.6"
55
+ }
56
+ }
package/pricing.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Hetzner pricing operations - fetch server types, locations, and calculate costs
3
+ */
4
+ import { z } from "zod";
5
+ import { HetznerListServerTypesResponseSchema, HetznerListLocationsResponseSchema, HetznerListDatacentersResponseSchema, } from "./schemas.js";
6
+ /**
7
+ * Default fallback price for unknown server types (EUR/month)
8
+ * Conservative estimate based on Hetzner's smallest standard server
9
+ */
10
+ const DEFAULT_FALLBACK_PRICE_MONTHLY = 5.0;
11
+ /**
12
+ * Hours per month for hourly cost calculation
13
+ * Using 730 hours (average month: 365.25 days * 24 hours / 12 months)
14
+ */
15
+ const HOURS_PER_MONTH = 730;
16
+ // ============================================================================
17
+ // Zod Schemas for Cost Calculation
18
+ // ============================================================================
19
+ /**
20
+ * Schema for server type price entry
21
+ */
22
+ export const ServerTypePriceSchema = z.object({
23
+ serverType: z.string(),
24
+ priceMonthly: z.number().nonnegative(),
25
+ priceHourly: z.number().nonnegative(),
26
+ deprecated: z.boolean().optional().default(false),
27
+ });
28
+ /**
29
+ * Schema for environment cost entry
30
+ */
31
+ export const EnvironmentCostSchema = z.object({
32
+ environmentId: z.string(),
33
+ environmentName: z.string(),
34
+ serverType: z.string(),
35
+ isRunning: z.boolean(),
36
+ costMonthly: z.number().nonnegative(),
37
+ costHourly: z.number().nonnegative(),
38
+ priceInfo: ServerTypePriceSchema.optional(),
39
+ });
40
+ /**
41
+ * Schema for cost calculation result
42
+ */
43
+ export const CostCalculationResultSchema = z.object({
44
+ totalMonthly: z.number().nonnegative(),
45
+ totalHourly: z.number().nonnegative(),
46
+ runningEnvironmentCount: z.number().nonnegative().int(),
47
+ stoppedEnvironmentCount: z.number().nonnegative().int(),
48
+ unknownServerTypeCount: z.number().nonnegative().int(),
49
+ breakdown: z.array(EnvironmentCostSchema),
50
+ priceMap: z.custom(),
51
+ });
52
+ // ============================================================================
53
+ // Cost Calculation Utilities
54
+ // ============================================================================
55
+ /**
56
+ * Parse Hetzner price string to number (EUR)
57
+ * Hetzner returns prices as strings in gross format (e.g., "5.3400" for 5.34 EUR)
58
+ *
59
+ * @param priceString - Price string from Hetzner API (gross field)
60
+ * @returns Price in EUR as number
61
+ */
62
+ export function parseHetznerPrice(priceString) {
63
+ const parsed = parseFloat(priceString);
64
+ if (isNaN(parsed)) {
65
+ console.warn(`Failed to parse Hetzner price string: ${priceString}`);
66
+ return 0;
67
+ }
68
+ return parsed;
69
+ }
70
+ /**
71
+ * Extract pricing information from a Hetzner server type
72
+ * Uses the first price entry (location-agnostic pricing)
73
+ *
74
+ * @param serverType - Hetzner server type object
75
+ * @returns Server type price info or undefined if pricing unavailable
76
+ */
77
+ export function extractServerTypePrice(serverType) {
78
+ if (!serverType.prices || serverType.prices.length === 0) {
79
+ console.warn(`Server type ${serverType.name} has no pricing information`);
80
+ return undefined;
81
+ }
82
+ // Use first price entry (Hetzner API returns location-agnostic pricing first)
83
+ const priceEntry = serverType.prices[0];
84
+ const priceMonthly = parseHetznerPrice(priceEntry.price_monthly.gross);
85
+ const priceHourly = parseHetznerPrice(priceEntry.price_hourly.gross);
86
+ return {
87
+ serverType: serverType.name,
88
+ priceMonthly,
89
+ priceHourly,
90
+ deprecated: serverType.deprecated ?? false,
91
+ };
92
+ }
93
+ /**
94
+ * Build a price lookup map from server types
95
+ *
96
+ * @param serverTypes - Array of Hetzner server types
97
+ * @returns Map of server type name to price info
98
+ */
99
+ export function buildPriceMap(serverTypes) {
100
+ const priceMap = new Map();
101
+ for (const serverType of serverTypes) {
102
+ const priceInfo = extractServerTypePrice(serverType);
103
+ if (priceInfo) {
104
+ priceMap.set(serverType.name, priceInfo);
105
+ }
106
+ }
107
+ return priceMap;
108
+ }
109
+ /**
110
+ * Get the monthly price for a server type from the price map
111
+ * Returns fallback price for unknown/deprecated types
112
+ *
113
+ * @param serverTypeName - Name of the server type
114
+ * @param priceMap - Price lookup map
115
+ * @param fallbackPrice - Fallback price for unknown types (default: 5.0 EUR)
116
+ * @returns Monthly price in EUR
117
+ */
118
+ export function getServerTypeMonthlyPrice(serverTypeName, priceMap, fallbackPrice = DEFAULT_FALLBACK_PRICE_MONTHLY) {
119
+ const priceInfo = priceMap.get(serverTypeName);
120
+ return priceInfo?.priceMonthly ?? fallbackPrice;
121
+ }
122
+ /**
123
+ * Calculate hourly price from monthly price
124
+ * Uses standard 730 hours per month
125
+ *
126
+ * @param monthlyPrice - Monthly price in EUR
127
+ * @returns Hourly price in EUR
128
+ */
129
+ export function calculateHourlyFromMonthly(monthlyPrice) {
130
+ return monthlyPrice / HOURS_PER_MONTH;
131
+ }
132
+ // ============================================================================
133
+ // Main Cost Calculation Function
134
+ // ============================================================================
135
+ /**
136
+ * Calculate total costs for a list of environments
137
+ *
138
+ * This function:
139
+ * - Only includes environments with "running" status
140
+ * - Handles missing/deprecated server types with fallback pricing
141
+ * - Returns detailed breakdown and aggregated totals
142
+ *
143
+ * @param environments - Array of environment objects
144
+ * @param serverTypes - Array of Hetzner server types with pricing
145
+ * @param options - Optional configuration
146
+ * @returns Cost calculation result with totals and breakdown
147
+ */
148
+ export function calculateCosts(environments, serverTypes, options = {}) {
149
+ const { fallbackPrice = DEFAULT_FALLBACK_PRICE_MONTHLY, includeStopped = false } = options;
150
+ // Build price lookup map
151
+ const priceMap = buildPriceMap(serverTypes);
152
+ // Calculate costs for each environment
153
+ const breakdown = [];
154
+ let totalMonthly = 0;
155
+ let runningCount = 0;
156
+ let stoppedCount = 0;
157
+ let unknownTypeCount = 0;
158
+ for (const env of environments) {
159
+ const isRunning = env.status === "running";
160
+ const priceInfo = priceMap.get(env.serverType);
161
+ const isUnknownType = !priceInfo;
162
+ // Count environments by status
163
+ if (isRunning) {
164
+ runningCount++;
165
+ }
166
+ else {
167
+ stoppedCount++;
168
+ }
169
+ // Count unknown server types (only for running environments)
170
+ if (isRunning && isUnknownType) {
171
+ unknownTypeCount++;
172
+ }
173
+ // Get price (use fallback for unknown types, or 0 for stopped environments)
174
+ const monthlyPrice = isRunning
175
+ ? (priceInfo?.priceMonthly ?? fallbackPrice)
176
+ : 0;
177
+ const hourlyPrice = isRunning
178
+ ? (priceInfo?.priceHourly ?? calculateHourlyFromMonthly(fallbackPrice))
179
+ : 0;
180
+ // Add to totals (only running environments)
181
+ if (isRunning) {
182
+ totalMonthly += monthlyPrice;
183
+ }
184
+ // Include in breakdown if running or if includeStopped is true
185
+ if (isRunning || includeStopped) {
186
+ breakdown.push({
187
+ environmentId: env.id,
188
+ environmentName: env.name,
189
+ serverType: env.serverType,
190
+ isRunning,
191
+ costMonthly: monthlyPrice,
192
+ costHourly: hourlyPrice,
193
+ priceInfo: priceInfo,
194
+ });
195
+ }
196
+ }
197
+ // Calculate total hourly from monthly total
198
+ const totalHourly = calculateHourlyFromMonthly(totalMonthly);
199
+ return {
200
+ totalMonthly,
201
+ totalHourly,
202
+ runningEnvironmentCount: runningCount,
203
+ stoppedEnvironmentCount: stoppedCount,
204
+ unknownServerTypeCount: unknownTypeCount,
205
+ breakdown,
206
+ priceMap,
207
+ };
208
+ }
209
+ // ============================================================================
210
+ // Pricing Operations Class
211
+ // ============================================================================
212
+ export class PricingOperations {
213
+ client;
214
+ constructor(client) {
215
+ this.client = client;
216
+ }
217
+ /**
218
+ * List all server types
219
+ */
220
+ async listServerTypes() {
221
+ const response = await this.client.request("/server_types");
222
+ // Validate response with Zod
223
+ const validated = HetznerListServerTypesResponseSchema.safeParse(response);
224
+ if (!validated.success) {
225
+ console.warn('Hetzner list server types validation warning:', validated.error.issues);
226
+ return response.server_types; // Return unvalidated data for backward compatibility
227
+ }
228
+ return validated.data.server_types;
229
+ }
230
+ /**
231
+ * Get a specific server type by name
232
+ */
233
+ async getServerType(name) {
234
+ const types = await this.listServerTypes();
235
+ return types.find(t => t.name === name);
236
+ }
237
+ /**
238
+ * List all locations
239
+ */
240
+ async listLocations() {
241
+ const response = await this.client.request("/locations");
242
+ // Validate response with Zod
243
+ const validated = HetznerListLocationsResponseSchema.safeParse(response);
244
+ if (!validated.success) {
245
+ console.warn('Hetzner list locations validation warning:', validated.error.issues);
246
+ return response.locations; // Return unvalidated data for backward compatibility
247
+ }
248
+ return validated.data.locations;
249
+ }
250
+ /**
251
+ * Get a specific location by name
252
+ */
253
+ async getLocation(name) {
254
+ const locations = await this.listLocations();
255
+ return locations.find(l => l.name === name);
256
+ }
257
+ /**
258
+ * List all datacenters
259
+ */
260
+ async listDatacenters() {
261
+ const response = await this.client.request("/datacenters");
262
+ // Validate response with Zod
263
+ const validated = HetznerListDatacentersResponseSchema.safeParse(response);
264
+ if (!validated.success) {
265
+ console.warn('Hetzner list datacenters validation warning:', validated.error.issues);
266
+ return response.datacenters; // Return unvalidated data for backward compatibility
267
+ }
268
+ return validated.data.datacenters;
269
+ }
270
+ /**
271
+ * Calculate costs for environments using current Hetzner pricing
272
+ *
273
+ * Convenience method that fetches server types and calculates costs in one call
274
+ *
275
+ * @param environments - Array of environment objects
276
+ * @param options - Optional configuration for cost calculation
277
+ * @returns Cost calculation result
278
+ */
279
+ async calculateEnvironmentCosts(environments, options) {
280
+ const serverTypes = await this.listServerTypes();
281
+ return calculateCosts(environments, serverTypes, options);
282
+ }
283
+ }
284
+ //# sourceMappingURL=pricing.js.map