s3db.js 13.4.0 → 13.6.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 (110) hide show
  1. package/README.md +25 -10
  2. package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
  3. package/dist/s3db.cjs.map +1 -0
  4. package/dist/s3db.es.js +38653 -32291
  5. package/dist/s3db.es.js.map +1 -1
  6. package/package.json +218 -22
  7. package/src/concerns/id.js +90 -6
  8. package/src/concerns/index.js +2 -1
  9. package/src/concerns/password-hashing.js +150 -0
  10. package/src/database.class.js +6 -2
  11. package/src/plugins/api/auth/basic-auth.js +40 -10
  12. package/src/plugins/api/auth/index.js +49 -3
  13. package/src/plugins/api/auth/oauth2-auth.js +171 -0
  14. package/src/plugins/api/auth/oidc-auth.js +789 -0
  15. package/src/plugins/api/auth/oidc-client.js +462 -0
  16. package/src/plugins/api/auth/path-auth-matcher.js +284 -0
  17. package/src/plugins/api/concerns/event-emitter.js +134 -0
  18. package/src/plugins/api/concerns/failban-manager.js +651 -0
  19. package/src/plugins/api/concerns/guards-helpers.js +402 -0
  20. package/src/plugins/api/concerns/metrics-collector.js +346 -0
  21. package/src/plugins/api/index.js +510 -57
  22. package/src/plugins/api/middlewares/failban.js +305 -0
  23. package/src/plugins/api/middlewares/rate-limit.js +301 -0
  24. package/src/plugins/api/middlewares/request-id.js +74 -0
  25. package/src/plugins/api/middlewares/security-headers.js +120 -0
  26. package/src/plugins/api/middlewares/session-tracking.js +194 -0
  27. package/src/plugins/api/routes/auth-routes.js +119 -78
  28. package/src/plugins/api/routes/resource-routes.js +73 -30
  29. package/src/plugins/api/server.js +1139 -45
  30. package/src/plugins/api/utils/custom-routes.js +102 -0
  31. package/src/plugins/api/utils/guards.js +213 -0
  32. package/src/plugins/api/utils/mime-types.js +154 -0
  33. package/src/plugins/api/utils/openapi-generator.js +91 -12
  34. package/src/plugins/api/utils/path-matcher.js +173 -0
  35. package/src/plugins/api/utils/static-filesystem.js +262 -0
  36. package/src/plugins/api/utils/static-s3.js +231 -0
  37. package/src/plugins/api/utils/template-engine.js +188 -0
  38. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
  39. package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
  40. package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
  41. package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
  42. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
  43. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
  44. package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
  45. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
  46. package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
  47. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
  48. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
  49. package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
  50. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
  51. package/src/plugins/cloud-inventory/index.js +20 -0
  52. package/src/plugins/cloud-inventory/registry.js +146 -0
  53. package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
  54. package/src/plugins/cloud-inventory.plugin.js +1333 -0
  55. package/src/plugins/concerns/plugin-dependencies.js +62 -2
  56. package/src/plugins/eventual-consistency/analytics.js +1 -0
  57. package/src/plugins/eventual-consistency/consolidation.js +2 -2
  58. package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
  59. package/src/plugins/eventual-consistency/install.js +2 -2
  60. package/src/plugins/identity/README.md +335 -0
  61. package/src/plugins/identity/concerns/mfa-manager.js +204 -0
  62. package/src/plugins/identity/concerns/password.js +138 -0
  63. package/src/plugins/identity/concerns/resource-schemas.js +273 -0
  64. package/src/plugins/identity/concerns/token-generator.js +172 -0
  65. package/src/plugins/identity/email-service.js +422 -0
  66. package/src/plugins/identity/index.js +1052 -0
  67. package/src/plugins/identity/oauth2-server.js +1033 -0
  68. package/src/plugins/identity/oidc-discovery.js +285 -0
  69. package/src/plugins/identity/rsa-keys.js +323 -0
  70. package/src/plugins/identity/server.js +500 -0
  71. package/src/plugins/identity/session-manager.js +453 -0
  72. package/src/plugins/identity/ui/layouts/base.js +251 -0
  73. package/src/plugins/identity/ui/middleware.js +135 -0
  74. package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
  75. package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
  76. package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
  77. package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
  78. package/src/plugins/identity/ui/pages/admin/users.js +263 -0
  79. package/src/plugins/identity/ui/pages/consent.js +262 -0
  80. package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
  81. package/src/plugins/identity/ui/pages/login.js +144 -0
  82. package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
  83. package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
  84. package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
  85. package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
  86. package/src/plugins/identity/ui/pages/profile.js +361 -0
  87. package/src/plugins/identity/ui/pages/register.js +226 -0
  88. package/src/plugins/identity/ui/pages/reset-password.js +128 -0
  89. package/src/plugins/identity/ui/pages/verify-email.js +172 -0
  90. package/src/plugins/identity/ui/routes.js +2541 -0
  91. package/src/plugins/identity/ui/styles/main.css +465 -0
  92. package/src/plugins/index.js +4 -1
  93. package/src/plugins/ml/base-model.class.js +65 -16
  94. package/src/plugins/ml/classification-model.class.js +1 -1
  95. package/src/plugins/ml/timeseries-model.class.js +3 -1
  96. package/src/plugins/ml.plugin.js +584 -31
  97. package/src/plugins/shared/error-handler.js +147 -0
  98. package/src/plugins/shared/index.js +9 -0
  99. package/src/plugins/shared/middlewares/compression.js +117 -0
  100. package/src/plugins/shared/middlewares/cors.js +49 -0
  101. package/src/plugins/shared/middlewares/index.js +11 -0
  102. package/src/plugins/shared/middlewares/logging.js +54 -0
  103. package/src/plugins/shared/middlewares/rate-limit.js +73 -0
  104. package/src/plugins/shared/middlewares/security.js +158 -0
  105. package/src/plugins/shared/response-formatter.js +264 -0
  106. package/src/plugins/state-machine.plugin.js +57 -2
  107. package/src/resource.class.js +140 -12
  108. package/src/schema.class.js +30 -1
  109. package/src/validator.class.js +57 -6
  110. package/dist/s3db.cjs.js.map +0 -1
@@ -0,0 +1,559 @@
1
+ import { BaseCloudDriver } from './base-driver.js';
2
+
3
+ /**
4
+ * Production-ready Hetzner Cloud inventory driver using hcloud-js library.
5
+ *
6
+ * Covers 12+ services with 15+ resource types:
7
+ * - Compute (servers/VPS, placement groups)
8
+ * - Storage (volumes)
9
+ * - Networking (networks, load balancers, firewalls, floating IPs, primary IPs)
10
+ * - SSH Keys, Images, Certificates, ISOs
11
+ *
12
+ * @see https://docs.hetzner.cloud/
13
+ * @see https://github.com/dennisbruner/hcloud-js
14
+ */
15
+ export class HetznerInventoryDriver extends BaseCloudDriver {
16
+ constructor(options = {}) {
17
+ super({ ...options, driver: options.driver || 'hetzner' });
18
+
19
+ this._apiToken = null;
20
+ this._client = null;
21
+ this._accountId = this.config?.accountId || 'hetzner';
22
+
23
+ // Services to collect (can be filtered via config.services)
24
+ this._services = this.config?.services || [
25
+ 'servers',
26
+ 'volumes',
27
+ 'networks',
28
+ 'loadbalancers',
29
+ 'firewalls',
30
+ 'floatingips',
31
+ 'sshkeys',
32
+ 'images',
33
+ 'certificates',
34
+ 'primaryips',
35
+ 'placementgroups',
36
+ 'isos'
37
+ ];
38
+ }
39
+
40
+ /**
41
+ * Initialize Hetzner Cloud API client.
42
+ */
43
+ async _initializeClient() {
44
+ if (this._client) return;
45
+
46
+ const credentials = this.credentials || {};
47
+ this._apiToken = credentials.token || credentials.apiToken || process.env.HETZNER_TOKEN;
48
+
49
+ if (!this._apiToken) {
50
+ throw new Error('Hetzner API token is required. Provide via credentials.token or HETZNER_TOKEN env var.');
51
+ }
52
+
53
+ // Lazy import to keep core package lightweight
54
+ const hcloud = await import('hcloud-js');
55
+ this._client = new hcloud.Client(this._apiToken);
56
+
57
+ this.logger('info', 'Hetzner Cloud API client initialized', {
58
+ accountId: this._accountId,
59
+ services: this._services.length
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Main entry point - lists all resources from configured services.
65
+ */
66
+ async *listResources(options = {}) {
67
+ await this._initializeClient();
68
+
69
+ const serviceCollectors = {
70
+ servers: () => this._collectServers(),
71
+ volumes: () => this._collectVolumes(),
72
+ networks: () => this._collectNetworks(),
73
+ loadbalancers: () => this._collectLoadBalancers(),
74
+ firewalls: () => this._collectFirewalls(),
75
+ floatingips: () => this._collectFloatingIPs(),
76
+ sshkeys: () => this._collectSSHKeys(),
77
+ images: () => this._collectImages(),
78
+ certificates: () => this._collectCertificates(),
79
+ primaryips: () => this._collectPrimaryIPs(),
80
+ placementgroups: () => this._collectPlacementGroups(),
81
+ isos: () => this._collectISOs()
82
+ };
83
+
84
+ for (const service of this._services) {
85
+ const collector = serviceCollectors[service];
86
+ if (!collector) {
87
+ this.logger('warn', `Unknown Hetzner service: ${service}`, { service });
88
+ continue;
89
+ }
90
+
91
+ try {
92
+ this.logger('info', `Collecting Hetzner ${service} resources`, { service });
93
+ yield* collector();
94
+ } catch (err) {
95
+ // Continue with next service instead of failing entire sync
96
+ this.logger('error', `Hetzner service collection failed, skipping to next service`, {
97
+ service,
98
+ error: err.message,
99
+ errorName: err.name,
100
+ stack: err.stack
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Collect Servers (VPS).
108
+ */
109
+ async *_collectServers() {
110
+ try {
111
+ const response = await this._client.servers.list();
112
+ const servers = response.servers || [];
113
+
114
+ for (const server of servers) {
115
+ yield {
116
+ provider: 'hetzner',
117
+ accountId: this._accountId,
118
+ region: server.datacenter?.location?.name || null,
119
+ service: 'servers',
120
+ resourceType: 'hetzner.server',
121
+ resourceId: server.id?.toString(),
122
+ name: server.name,
123
+ tags: this._extractLabels(server.labels),
124
+ configuration: this._sanitize(server)
125
+ };
126
+ }
127
+
128
+ this.logger('info', `Collected ${servers.length} Hetzner servers`);
129
+ } catch (err) {
130
+ this.logger('error', 'Failed to collect Hetzner servers', {
131
+ error: err.message,
132
+ stack: err.stack
133
+ });
134
+ throw err;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Collect Volumes (block storage).
140
+ */
141
+ async *_collectVolumes() {
142
+ try {
143
+ const response = await this._client.volumes.list();
144
+ const volumes = response.volumes || [];
145
+
146
+ for (const volume of volumes) {
147
+ yield {
148
+ provider: 'hetzner',
149
+ accountId: this._accountId,
150
+ region: volume.location?.name || null,
151
+ service: 'volumes',
152
+ resourceType: 'hetzner.volume',
153
+ resourceId: volume.id?.toString(),
154
+ name: volume.name,
155
+ tags: this._extractLabels(volume.labels),
156
+ configuration: this._sanitize(volume)
157
+ };
158
+ }
159
+
160
+ this.logger('info', `Collected ${volumes.length} Hetzner volumes`);
161
+ } catch (err) {
162
+ this.logger('error', 'Failed to collect Hetzner volumes', {
163
+ error: err.message,
164
+ stack: err.stack
165
+ });
166
+ throw err;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Collect Networks (private networks/VPC).
172
+ */
173
+ async *_collectNetworks() {
174
+ try {
175
+ const response = await this._client.networks.list();
176
+ const networks = response.networks || [];
177
+
178
+ for (const network of networks) {
179
+ yield {
180
+ provider: 'hetzner',
181
+ accountId: this._accountId,
182
+ region: null, // Networks span multiple locations
183
+ service: 'networks',
184
+ resourceType: 'hetzner.network',
185
+ resourceId: network.id?.toString(),
186
+ name: network.name,
187
+ tags: this._extractLabels(network.labels),
188
+ configuration: this._sanitize(network)
189
+ };
190
+
191
+ // Subnets are embedded in network
192
+ if (network.subnets && Array.isArray(network.subnets)) {
193
+ for (const subnet of network.subnets) {
194
+ yield {
195
+ provider: 'hetzner',
196
+ accountId: this._accountId,
197
+ region: subnet.network_zone,
198
+ service: 'networks',
199
+ resourceType: 'hetzner.network.subnet',
200
+ resourceId: `${network.id}/subnet/${subnet.ip_range}`,
201
+ name: `${network.name}-${subnet.type}`,
202
+ tags: {},
203
+ metadata: { networkId: network.id, networkName: network.name },
204
+ configuration: this._sanitize(subnet)
205
+ };
206
+ }
207
+ }
208
+ }
209
+
210
+ this.logger('info', `Collected ${networks.length} Hetzner networks`);
211
+ } catch (err) {
212
+ this.logger('error', 'Failed to collect Hetzner networks', {
213
+ error: err.message,
214
+ stack: err.stack
215
+ });
216
+ throw err;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Collect Load Balancers.
222
+ */
223
+ async *_collectLoadBalancers() {
224
+ try {
225
+ const response = await this._client.loadBalancers.list();
226
+ const loadBalancers = response.load_balancers || [];
227
+
228
+ for (const lb of loadBalancers) {
229
+ yield {
230
+ provider: 'hetzner',
231
+ accountId: this._accountId,
232
+ region: lb.location?.name || null,
233
+ service: 'loadbalancers',
234
+ resourceType: 'hetzner.loadbalancer',
235
+ resourceId: lb.id?.toString(),
236
+ name: lb.name,
237
+ tags: this._extractLabels(lb.labels),
238
+ configuration: this._sanitize(lb)
239
+ };
240
+ }
241
+
242
+ this.logger('info', `Collected ${loadBalancers.length} Hetzner load balancers`);
243
+ } catch (err) {
244
+ this.logger('error', 'Failed to collect Hetzner load balancers', {
245
+ error: err.message,
246
+ stack: err.stack
247
+ });
248
+ throw err;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Collect Firewalls.
254
+ */
255
+ async *_collectFirewalls() {
256
+ try {
257
+ const response = await this._client.firewalls.list();
258
+ const firewalls = response.firewalls || [];
259
+
260
+ for (const firewall of firewalls) {
261
+ yield {
262
+ provider: 'hetzner',
263
+ accountId: this._accountId,
264
+ region: null, // Firewalls are global
265
+ service: 'firewalls',
266
+ resourceType: 'hetzner.firewall',
267
+ resourceId: firewall.id?.toString(),
268
+ name: firewall.name,
269
+ tags: this._extractLabels(firewall.labels),
270
+ configuration: this._sanitize(firewall)
271
+ };
272
+ }
273
+
274
+ this.logger('info', `Collected ${firewalls.length} Hetzner firewalls`);
275
+ } catch (err) {
276
+ this.logger('error', 'Failed to collect Hetzner firewalls', {
277
+ error: err.message,
278
+ stack: err.stack
279
+ });
280
+ throw err;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Collect Floating IPs.
286
+ */
287
+ async *_collectFloatingIPs() {
288
+ try {
289
+ const response = await this._client.floatingIPs.list();
290
+ const floatingIPs = response.floating_ips || [];
291
+
292
+ for (const fip of floatingIPs) {
293
+ yield {
294
+ provider: 'hetzner',
295
+ accountId: this._accountId,
296
+ region: fip.home_location?.name || null,
297
+ service: 'floatingips',
298
+ resourceType: 'hetzner.floatingip',
299
+ resourceId: fip.id?.toString(),
300
+ name: fip.name || fip.ip,
301
+ tags: this._extractLabels(fip.labels),
302
+ configuration: this._sanitize(fip)
303
+ };
304
+ }
305
+
306
+ this.logger('info', `Collected ${floatingIPs.length} Hetzner floating IPs`);
307
+ } catch (err) {
308
+ this.logger('error', 'Failed to collect Hetzner floating IPs', {
309
+ error: err.message,
310
+ stack: err.stack
311
+ });
312
+ throw err;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Collect SSH Keys.
318
+ */
319
+ async *_collectSSHKeys() {
320
+ try {
321
+ const response = await this._client.sshKeys.list();
322
+ const sshKeys = response.ssh_keys || [];
323
+
324
+ for (const key of sshKeys) {
325
+ yield {
326
+ provider: 'hetzner',
327
+ accountId: this._accountId,
328
+ region: null, // SSH keys are global
329
+ service: 'sshkeys',
330
+ resourceType: 'hetzner.sshkey',
331
+ resourceId: key.id?.toString(),
332
+ name: key.name,
333
+ tags: this._extractLabels(key.labels),
334
+ configuration: this._sanitize(key)
335
+ };
336
+ }
337
+
338
+ this.logger('info', `Collected ${sshKeys.length} Hetzner SSH keys`);
339
+ } catch (err) {
340
+ this.logger('error', 'Failed to collect Hetzner SSH keys', {
341
+ error: err.message,
342
+ stack: err.stack
343
+ });
344
+ throw err;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Collect custom Images.
350
+ */
351
+ async *_collectImages() {
352
+ try {
353
+ const response = await this._client.images.list();
354
+ const images = response.images || [];
355
+
356
+ // Filter to only custom images (type = 'snapshot' or 'backup')
357
+ const customImages = images.filter(img => img.type === 'snapshot' || img.type === 'backup');
358
+
359
+ for (const image of customImages) {
360
+ yield {
361
+ provider: 'hetzner',
362
+ accountId: this._accountId,
363
+ region: null, // Images are global
364
+ service: 'images',
365
+ resourceType: 'hetzner.image',
366
+ resourceId: image.id?.toString(),
367
+ name: image.description || image.name || image.id?.toString(),
368
+ tags: this._extractLabels(image.labels),
369
+ configuration: this._sanitize(image)
370
+ };
371
+ }
372
+
373
+ this.logger('info', `Collected ${customImages.length} custom Hetzner images`);
374
+ } catch (err) {
375
+ this.logger('error', 'Failed to collect Hetzner images', {
376
+ error: err.message,
377
+ stack: err.stack
378
+ });
379
+ throw err;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Collect SSL Certificates.
385
+ */
386
+ async *_collectCertificates() {
387
+ try {
388
+ const response = await this._client.certificates.list();
389
+ const certificates = response.certificates || [];
390
+
391
+ for (const cert of certificates) {
392
+ yield {
393
+ provider: 'hetzner',
394
+ accountId: this._accountId,
395
+ region: null, // Certificates are global
396
+ service: 'certificates',
397
+ resourceType: 'hetzner.certificate',
398
+ resourceId: cert.id?.toString(),
399
+ name: cert.name,
400
+ tags: this._extractLabels(cert.labels),
401
+ configuration: this._sanitize(cert)
402
+ };
403
+ }
404
+
405
+ this.logger('info', `Collected ${certificates.length} Hetzner certificates`);
406
+ } catch (err) {
407
+ this.logger('error', 'Failed to collect Hetzner certificates', {
408
+ error: err.message,
409
+ stack: err.stack
410
+ });
411
+ throw err;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Collect Primary IPs (independent public IPs).
417
+ */
418
+ async *_collectPrimaryIPs() {
419
+ try {
420
+ const response = await this._client.primaryIPs.list();
421
+ const primaryIPs = response.primary_ips || [];
422
+
423
+ for (const ip of primaryIPs) {
424
+ yield {
425
+ provider: 'hetzner',
426
+ accountId: this._accountId,
427
+ region: ip.datacenter?.location?.name || null,
428
+ service: 'primaryips',
429
+ resourceType: 'hetzner.primaryip',
430
+ resourceId: ip.id?.toString(),
431
+ name: ip.name || ip.ip,
432
+ tags: this._extractLabels(ip.labels),
433
+ metadata: {
434
+ type: ip.type, // ipv4 or ipv6
435
+ assignedToId: ip.assignee_id
436
+ },
437
+ configuration: this._sanitize(ip)
438
+ };
439
+ }
440
+
441
+ this.logger('info', `Collected ${primaryIPs.length} Hetzner primary IPs`);
442
+ } catch (err) {
443
+ this.logger('error', 'Failed to collect Hetzner primary IPs', {
444
+ error: err.message,
445
+ stack: err.stack
446
+ });
447
+ throw err;
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Collect Placement Groups (server anti-affinity).
453
+ */
454
+ async *_collectPlacementGroups() {
455
+ try {
456
+ const response = await this._client.placementGroups.list();
457
+ const placementGroups = response.placement_groups || [];
458
+
459
+ for (const pg of placementGroups) {
460
+ yield {
461
+ provider: 'hetzner',
462
+ accountId: this._accountId,
463
+ region: null, // Placement groups are global
464
+ service: 'placementgroups',
465
+ resourceType: 'hetzner.placementgroup',
466
+ resourceId: pg.id?.toString(),
467
+ name: pg.name,
468
+ tags: this._extractLabels(pg.labels),
469
+ metadata: {
470
+ type: pg.type,
471
+ servers: pg.servers || []
472
+ },
473
+ configuration: this._sanitize(pg)
474
+ };
475
+ }
476
+
477
+ this.logger('info', `Collected ${placementGroups.length} Hetzner placement groups`);
478
+ } catch (err) {
479
+ this.logger('error', 'Failed to collect Hetzner placement groups', {
480
+ error: err.message,
481
+ stack: err.stack
482
+ });
483
+ throw err;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Collect ISOs (custom installation images).
489
+ */
490
+ async *_collectISOs() {
491
+ try {
492
+ const response = await this._client.isos.list();
493
+ const isos = response.isos || [];
494
+
495
+ for (const iso of isos) {
496
+ yield {
497
+ provider: 'hetzner',
498
+ accountId: this._accountId,
499
+ region: null, // ISOs are global
500
+ service: 'isos',
501
+ resourceType: 'hetzner.iso',
502
+ resourceId: iso.id?.toString(),
503
+ name: iso.name,
504
+ tags: {},
505
+ metadata: {
506
+ type: iso.type,
507
+ deprecated: iso.deprecated,
508
+ deprecation: iso.deprecation
509
+ },
510
+ configuration: this._sanitize(iso)
511
+ };
512
+ }
513
+
514
+ this.logger('info', `Collected ${isos.length} Hetzner ISOs`);
515
+ } catch (err) {
516
+ this.logger('error', 'Failed to collect Hetzner ISOs', {
517
+ error: err.message,
518
+ stack: err.stack
519
+ });
520
+ throw err;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Extract labels from Hetzner labels object.
526
+ */
527
+ _extractLabels(labels) {
528
+ if (!labels || typeof labels !== 'object') return {};
529
+ return { ...labels };
530
+ }
531
+
532
+ /**
533
+ * Sanitize configuration by removing sensitive data.
534
+ */
535
+ _sanitize(config) {
536
+ if (!config || typeof config !== 'object') return config;
537
+
538
+ const sanitized = { ...config };
539
+ const sensitiveFields = [
540
+ 'root_password',
541
+ 'password',
542
+ 'token',
543
+ 'secret',
544
+ 'api_key',
545
+ 'private_key',
546
+ 'public_key',
547
+ 'certificate',
548
+ 'private_key'
549
+ ];
550
+
551
+ for (const field of sensitiveFields) {
552
+ if (field in sanitized) {
553
+ sanitized[field] = '***REDACTED***';
554
+ }
555
+ }
556
+
557
+ return sanitized;
558
+ }
559
+ }