@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/pricing.ts
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hetzner pricing operations - fetch server types, locations, and calculate costs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type {
|
|
7
|
+
HetznerServerType,
|
|
8
|
+
HetznerLocation,
|
|
9
|
+
HetznerDatacenter,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import type { HetznerClient } from "./client.js";
|
|
12
|
+
import type { Environment } from "@ebowwa/codespaces-types/compile";
|
|
13
|
+
import {
|
|
14
|
+
HetznerListServerTypesResponseSchema,
|
|
15
|
+
HetznerListLocationsResponseSchema,
|
|
16
|
+
HetznerListDatacentersResponseSchema,
|
|
17
|
+
} from "./schemas.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Cost Calculation Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Price information for a server type (in EUR)
|
|
25
|
+
*/
|
|
26
|
+
export interface ServerTypePrice {
|
|
27
|
+
/** Server type name (e.g., "cpx11") */
|
|
28
|
+
serverType: string;
|
|
29
|
+
/** Monthly price in EUR */
|
|
30
|
+
priceMonthly: number;
|
|
31
|
+
/** Hourly price in EUR */
|
|
32
|
+
priceHourly: number;
|
|
33
|
+
/** Whether the server type is deprecated */
|
|
34
|
+
deprecated: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cost breakdown for a single environment
|
|
39
|
+
*/
|
|
40
|
+
export interface EnvironmentCost {
|
|
41
|
+
/** Environment ID */
|
|
42
|
+
environmentId: string;
|
|
43
|
+
/** Environment name */
|
|
44
|
+
environmentName: string;
|
|
45
|
+
/** Server type name */
|
|
46
|
+
serverType: string;
|
|
47
|
+
/** Whether the environment is currently running */
|
|
48
|
+
isRunning: boolean;
|
|
49
|
+
/** Monthly cost in EUR */
|
|
50
|
+
costMonthly: number;
|
|
51
|
+
/** Hourly cost in EUR */
|
|
52
|
+
costHourly: number;
|
|
53
|
+
/** Price info (undefined if server type not found) */
|
|
54
|
+
priceInfo?: ServerTypePrice;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Total cost calculation result
|
|
59
|
+
*/
|
|
60
|
+
export interface CostCalculationResult {
|
|
61
|
+
/** Total monthly cost for all running environments (EUR) */
|
|
62
|
+
totalMonthly: number;
|
|
63
|
+
/** Total hourly cost for all running environments (EUR) */
|
|
64
|
+
totalHourly: number;
|
|
65
|
+
/** Number of environments included in calculation */
|
|
66
|
+
runningEnvironmentCount: number;
|
|
67
|
+
/** Number of environments excluded (not running) */
|
|
68
|
+
stoppedEnvironmentCount: number;
|
|
69
|
+
/** Number of environments with unknown server types */
|
|
70
|
+
unknownServerTypeCount: number;
|
|
71
|
+
/** Detailed breakdown by environment */
|
|
72
|
+
breakdown: EnvironmentCost[];
|
|
73
|
+
/** Map of server type names to their prices (for reference) */
|
|
74
|
+
priceMap: Map<string, ServerTypePrice>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default fallback price for unknown server types (EUR/month)
|
|
79
|
+
* Conservative estimate based on Hetzner's smallest standard server
|
|
80
|
+
*/
|
|
81
|
+
const DEFAULT_FALLBACK_PRICE_MONTHLY = 5.0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hours per month for hourly cost calculation
|
|
85
|
+
* Using 730 hours (average month: 365.25 days * 24 hours / 12 months)
|
|
86
|
+
*/
|
|
87
|
+
const HOURS_PER_MONTH = 730;
|
|
88
|
+
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Zod Schemas for Cost Calculation
|
|
91
|
+
// ============================================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Schema for server type price entry
|
|
95
|
+
*/
|
|
96
|
+
export const ServerTypePriceSchema = z.object({
|
|
97
|
+
serverType: z.string(),
|
|
98
|
+
priceMonthly: z.number().nonnegative(),
|
|
99
|
+
priceHourly: z.number().nonnegative(),
|
|
100
|
+
deprecated: z.boolean().optional().default(false),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Schema for environment cost entry
|
|
105
|
+
*/
|
|
106
|
+
export const EnvironmentCostSchema = z.object({
|
|
107
|
+
environmentId: z.string(),
|
|
108
|
+
environmentName: z.string(),
|
|
109
|
+
serverType: z.string(),
|
|
110
|
+
isRunning: z.boolean(),
|
|
111
|
+
costMonthly: z.number().nonnegative(),
|
|
112
|
+
costHourly: z.number().nonnegative(),
|
|
113
|
+
priceInfo: ServerTypePriceSchema.optional(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Schema for cost calculation result
|
|
118
|
+
*/
|
|
119
|
+
export const CostCalculationResultSchema = z.object({
|
|
120
|
+
totalMonthly: z.number().nonnegative(),
|
|
121
|
+
totalHourly: z.number().nonnegative(),
|
|
122
|
+
runningEnvironmentCount: z.number().nonnegative().int(),
|
|
123
|
+
stoppedEnvironmentCount: z.number().nonnegative().int(),
|
|
124
|
+
unknownServerTypeCount: z.number().nonnegative().int(),
|
|
125
|
+
breakdown: z.array(EnvironmentCostSchema),
|
|
126
|
+
priceMap: z.custom<Map<string, ServerTypePrice>>(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Cost Calculation Utilities
|
|
131
|
+
// ============================================================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Parse Hetzner price string to number (EUR)
|
|
135
|
+
* Hetzner returns prices as strings in gross format (e.g., "5.3400" for 5.34 EUR)
|
|
136
|
+
*
|
|
137
|
+
* @param priceString - Price string from Hetzner API (gross field)
|
|
138
|
+
* @returns Price in EUR as number
|
|
139
|
+
*/
|
|
140
|
+
export function parseHetznerPrice(priceString: string): number {
|
|
141
|
+
const parsed = parseFloat(priceString);
|
|
142
|
+
if (isNaN(parsed)) {
|
|
143
|
+
console.warn(`Failed to parse Hetzner price string: ${priceString}`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract pricing information from a Hetzner server type
|
|
151
|
+
* Uses the first price entry (location-agnostic pricing)
|
|
152
|
+
*
|
|
153
|
+
* @param serverType - Hetzner server type object
|
|
154
|
+
* @returns Server type price info or undefined if pricing unavailable
|
|
155
|
+
*/
|
|
156
|
+
export function extractServerTypePrice(
|
|
157
|
+
serverType: HetznerServerType,
|
|
158
|
+
): ServerTypePrice | undefined {
|
|
159
|
+
if (!serverType.prices || serverType.prices.length === 0) {
|
|
160
|
+
console.warn(`Server type ${serverType.name} has no pricing information`);
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Use first price entry (Hetzner API returns location-agnostic pricing first)
|
|
165
|
+
const priceEntry = serverType.prices[0];
|
|
166
|
+
|
|
167
|
+
const priceMonthly = parseHetznerPrice(priceEntry.price_monthly.gross);
|
|
168
|
+
const priceHourly = parseHetznerPrice(priceEntry.price_hourly.gross);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
serverType: serverType.name,
|
|
172
|
+
priceMonthly,
|
|
173
|
+
priceHourly,
|
|
174
|
+
deprecated: serverType.deprecated ?? false,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build a price lookup map from server types
|
|
180
|
+
*
|
|
181
|
+
* @param serverTypes - Array of Hetzner server types
|
|
182
|
+
* @returns Map of server type name to price info
|
|
183
|
+
*/
|
|
184
|
+
export function buildPriceMap(
|
|
185
|
+
serverTypes: HetznerServerType[],
|
|
186
|
+
): Map<string, ServerTypePrice> {
|
|
187
|
+
const priceMap = new Map<string, ServerTypePrice>();
|
|
188
|
+
|
|
189
|
+
for (const serverType of serverTypes) {
|
|
190
|
+
const priceInfo = extractServerTypePrice(serverType);
|
|
191
|
+
if (priceInfo) {
|
|
192
|
+
priceMap.set(serverType.name, priceInfo);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return priceMap;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get the monthly price for a server type from the price map
|
|
201
|
+
* Returns fallback price for unknown/deprecated types
|
|
202
|
+
*
|
|
203
|
+
* @param serverTypeName - Name of the server type
|
|
204
|
+
* @param priceMap - Price lookup map
|
|
205
|
+
* @param fallbackPrice - Fallback price for unknown types (default: 5.0 EUR)
|
|
206
|
+
* @returns Monthly price in EUR
|
|
207
|
+
*/
|
|
208
|
+
export function getServerTypeMonthlyPrice(
|
|
209
|
+
serverTypeName: string,
|
|
210
|
+
priceMap: Map<string, ServerTypePrice>,
|
|
211
|
+
fallbackPrice: number = DEFAULT_FALLBACK_PRICE_MONTHLY,
|
|
212
|
+
): number {
|
|
213
|
+
const priceInfo = priceMap.get(serverTypeName);
|
|
214
|
+
return priceInfo?.priceMonthly ?? fallbackPrice;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Calculate hourly price from monthly price
|
|
219
|
+
* Uses standard 730 hours per month
|
|
220
|
+
*
|
|
221
|
+
* @param monthlyPrice - Monthly price in EUR
|
|
222
|
+
* @returns Hourly price in EUR
|
|
223
|
+
*/
|
|
224
|
+
export function calculateHourlyFromMonthly(monthlyPrice: number): number {
|
|
225
|
+
return monthlyPrice / HOURS_PER_MONTH;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// Main Cost Calculation Function
|
|
230
|
+
// ============================================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Calculate total costs for a list of environments
|
|
234
|
+
*
|
|
235
|
+
* This function:
|
|
236
|
+
* - Only includes environments with "running" status
|
|
237
|
+
* - Handles missing/deprecated server types with fallback pricing
|
|
238
|
+
* - Returns detailed breakdown and aggregated totals
|
|
239
|
+
*
|
|
240
|
+
* @param environments - Array of environment objects
|
|
241
|
+
* @param serverTypes - Array of Hetzner server types with pricing
|
|
242
|
+
* @param options - Optional configuration
|
|
243
|
+
* @returns Cost calculation result with totals and breakdown
|
|
244
|
+
*/
|
|
245
|
+
export function calculateCosts(
|
|
246
|
+
environments: Environment[],
|
|
247
|
+
serverTypes: HetznerServerType[],
|
|
248
|
+
options: {
|
|
249
|
+
/** Custom fallback price for unknown server types (EUR/month) */
|
|
250
|
+
fallbackPrice?: number;
|
|
251
|
+
/** Whether to include stopped environments in breakdown (default: false) */
|
|
252
|
+
includeStopped?: boolean;
|
|
253
|
+
} = {},
|
|
254
|
+
): CostCalculationResult {
|
|
255
|
+
const { fallbackPrice = DEFAULT_FALLBACK_PRICE_MONTHLY, includeStopped = false } =
|
|
256
|
+
options;
|
|
257
|
+
|
|
258
|
+
// Build price lookup map
|
|
259
|
+
const priceMap = buildPriceMap(serverTypes);
|
|
260
|
+
|
|
261
|
+
// Calculate costs for each environment
|
|
262
|
+
const breakdown: EnvironmentCost[] = [];
|
|
263
|
+
let totalMonthly = 0;
|
|
264
|
+
let runningCount = 0;
|
|
265
|
+
let stoppedCount = 0;
|
|
266
|
+
let unknownTypeCount = 0;
|
|
267
|
+
|
|
268
|
+
for (const env of environments) {
|
|
269
|
+
const isRunning = env.status === "running";
|
|
270
|
+
const priceInfo = priceMap.get(env.serverType);
|
|
271
|
+
const isUnknownType = !priceInfo;
|
|
272
|
+
|
|
273
|
+
// Count environments by status
|
|
274
|
+
if (isRunning) {
|
|
275
|
+
runningCount++;
|
|
276
|
+
} else {
|
|
277
|
+
stoppedCount++;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Count unknown server types (only for running environments)
|
|
281
|
+
if (isRunning && isUnknownType) {
|
|
282
|
+
unknownTypeCount++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Get price (use fallback for unknown types, or 0 for stopped environments)
|
|
286
|
+
const monthlyPrice = isRunning
|
|
287
|
+
? (priceInfo?.priceMonthly ?? fallbackPrice)
|
|
288
|
+
: 0;
|
|
289
|
+
const hourlyPrice = isRunning
|
|
290
|
+
? (priceInfo?.priceHourly ?? calculateHourlyFromMonthly(fallbackPrice))
|
|
291
|
+
: 0;
|
|
292
|
+
|
|
293
|
+
// Add to totals (only running environments)
|
|
294
|
+
if (isRunning) {
|
|
295
|
+
totalMonthly += monthlyPrice;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Include in breakdown if running or if includeStopped is true
|
|
299
|
+
if (isRunning || includeStopped) {
|
|
300
|
+
breakdown.push({
|
|
301
|
+
environmentId: env.id,
|
|
302
|
+
environmentName: env.name,
|
|
303
|
+
serverType: env.serverType,
|
|
304
|
+
isRunning,
|
|
305
|
+
costMonthly: monthlyPrice,
|
|
306
|
+
costHourly: hourlyPrice,
|
|
307
|
+
priceInfo: priceInfo,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Calculate total hourly from monthly total
|
|
313
|
+
const totalHourly = calculateHourlyFromMonthly(totalMonthly);
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
totalMonthly,
|
|
317
|
+
totalHourly,
|
|
318
|
+
runningEnvironmentCount: runningCount,
|
|
319
|
+
stoppedEnvironmentCount: stoppedCount,
|
|
320
|
+
unknownServerTypeCount: unknownTypeCount,
|
|
321
|
+
breakdown,
|
|
322
|
+
priceMap,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ============================================================================
|
|
327
|
+
// Pricing Operations Class
|
|
328
|
+
// ============================================================================
|
|
329
|
+
|
|
330
|
+
export class PricingOperations {
|
|
331
|
+
constructor(private client: HetznerClient) {}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* List all server types
|
|
335
|
+
*/
|
|
336
|
+
async listServerTypes(): Promise<HetznerServerType[]> {
|
|
337
|
+
const response = await this.client.request<{ server_types: HetznerServerType[] }>(
|
|
338
|
+
"/server_types"
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Validate response with Zod
|
|
342
|
+
const validated = HetznerListServerTypesResponseSchema.safeParse(response);
|
|
343
|
+
if (!validated.success) {
|
|
344
|
+
console.warn('Hetzner list server types validation warning:', validated.error.issues);
|
|
345
|
+
return response.server_types; // Return unvalidated data for backward compatibility
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return validated.data.server_types;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get a specific server type by name
|
|
353
|
+
*/
|
|
354
|
+
async getServerType(name: string): Promise<HetznerServerType | undefined> {
|
|
355
|
+
const types = await this.listServerTypes();
|
|
356
|
+
return types.find(t => t.name === name);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* List all locations
|
|
361
|
+
*/
|
|
362
|
+
async listLocations(): Promise<HetznerLocation[]> {
|
|
363
|
+
const response = await this.client.request<{ locations: HetznerLocation[] }>(
|
|
364
|
+
"/locations"
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Validate response with Zod
|
|
368
|
+
const validated = HetznerListLocationsResponseSchema.safeParse(response);
|
|
369
|
+
if (!validated.success) {
|
|
370
|
+
console.warn('Hetzner list locations validation warning:', validated.error.issues);
|
|
371
|
+
return response.locations; // Return unvalidated data for backward compatibility
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return validated.data.locations;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get a specific location by name
|
|
379
|
+
*/
|
|
380
|
+
async getLocation(name: string): Promise<HetznerLocation | undefined> {
|
|
381
|
+
const locations = await this.listLocations();
|
|
382
|
+
return locations.find(l => l.name === name);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* List all datacenters
|
|
387
|
+
*/
|
|
388
|
+
async listDatacenters(): Promise<HetznerDatacenter[]> {
|
|
389
|
+
const response = await this.client.request<{ datacenters: HetznerDatacenter[] }>(
|
|
390
|
+
"/datacenters"
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
// Validate response with Zod
|
|
394
|
+
const validated = HetznerListDatacentersResponseSchema.safeParse(response);
|
|
395
|
+
if (!validated.success) {
|
|
396
|
+
console.warn('Hetzner list datacenters validation warning:', validated.error.issues);
|
|
397
|
+
return response.datacenters; // Return unvalidated data for backward compatibility
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return validated.data.datacenters;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Calculate costs for environments using current Hetzner pricing
|
|
405
|
+
*
|
|
406
|
+
* Convenience method that fetches server types and calculates costs in one call
|
|
407
|
+
*
|
|
408
|
+
* @param environments - Array of environment objects
|
|
409
|
+
* @param options - Optional configuration for cost calculation
|
|
410
|
+
* @returns Cost calculation result
|
|
411
|
+
*/
|
|
412
|
+
async calculateEnvironmentCosts(
|
|
413
|
+
environments: Environment[],
|
|
414
|
+
options?: {
|
|
415
|
+
fallbackPrice?: number;
|
|
416
|
+
includeStopped?: boolean;
|
|
417
|
+
},
|
|
418
|
+
): Promise<CostCalculationResult> {
|
|
419
|
+
const serverTypes = await this.listServerTypes();
|
|
420
|
+
return calculateCosts(environments, serverTypes, options);
|
|
421
|
+
}
|
|
422
|
+
}
|