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,614 @@
1
+ import { BaseCloudDriver } from './base-driver.js';
2
+
3
+ /**
4
+ * Production-ready Linode (Akamai Cloud) inventory driver using official @linode/api-v4 SDK.
5
+ *
6
+ * Covers 12+ services with 18+ resource types:
7
+ * - Compute (Linodes/instances, placement groups)
8
+ * - Kubernetes (LKE clusters, node pools)
9
+ * - Storage (volumes, object storage buckets)
10
+ * - Networking (NodeBalancers, firewalls, VLANs)
11
+ * - DNS (domains, records)
12
+ * - Databases (managed MySQL, PostgreSQL, MongoDB)
13
+ * - Images, StackScripts
14
+ *
15
+ * @see https://www.linode.com/docs/api/
16
+ * @see https://www.npmjs.com/package/@linode/api-v4
17
+ */
18
+ export class LinodeInventoryDriver extends BaseCloudDriver {
19
+ constructor(options = {}) {
20
+ super({ ...options, driver: options.driver || 'linode' });
21
+
22
+ this._apiToken = null;
23
+ this._accountId = this.config?.accountId || 'linode';
24
+
25
+ // Services to collect (can be filtered via config.services)
26
+ this._services = this.config?.services || [
27
+ 'linodes',
28
+ 'kubernetes',
29
+ 'volumes',
30
+ 'nodebalancers',
31
+ 'firewalls',
32
+ 'vlans',
33
+ 'domains',
34
+ 'images',
35
+ 'objectstorage',
36
+ 'databases',
37
+ 'stackscripts',
38
+ 'placementgroups'
39
+ ];
40
+
41
+ // Regions to scan (null = all)
42
+ this._regions = this.config?.regions || null;
43
+ }
44
+
45
+ /**
46
+ * Initialize Linode API client.
47
+ */
48
+ async _initializeClient() {
49
+ if (this._apiToken) return;
50
+
51
+ const credentials = this.credentials || {};
52
+ this._apiToken = credentials.token || credentials.apiToken || process.env.LINODE_TOKEN;
53
+
54
+ if (!this._apiToken) {
55
+ throw new Error('Linode API token is required. Provide via credentials.token or LINODE_TOKEN env var.');
56
+ }
57
+
58
+ // Import and set token
59
+ const { setToken } = await import('@linode/api-v4');
60
+ setToken(this._apiToken);
61
+
62
+ this.logger('info', 'Linode API client initialized', {
63
+ accountId: this._accountId,
64
+ services: this._services.length
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Main entry point - lists all resources from configured services.
70
+ */
71
+ async *listResources(options = {}) {
72
+ await this._initializeClient();
73
+
74
+ const serviceCollectors = {
75
+ linodes: () => this._collectLinodes(),
76
+ kubernetes: () => this._collectKubernetes(),
77
+ volumes: () => this._collectVolumes(),
78
+ nodebalancers: () => this._collectNodeBalancers(),
79
+ firewalls: () => this._collectFirewalls(),
80
+ vlans: () => this._collectVLANs(),
81
+ domains: () => this._collectDomains(),
82
+ images: () => this._collectImages(),
83
+ objectstorage: () => this._collectObjectStorage(),
84
+ databases: () => this._collectDatabases(),
85
+ stackscripts: () => this._collectStackScripts(),
86
+ placementgroups: () => this._collectPlacementGroups()
87
+ };
88
+
89
+ for (const service of this._services) {
90
+ const collector = serviceCollectors[service];
91
+ if (!collector) {
92
+ this.logger('warn', `Unknown Linode service: ${service}`, { service });
93
+ continue;
94
+ }
95
+
96
+ try {
97
+ this.logger('info', `Collecting Linode ${service} resources`, { service });
98
+ yield* collector();
99
+ } catch (err) {
100
+ // Continue with next service instead of failing entire sync
101
+ this.logger('error', `Linode service collection failed, skipping to next service`, {
102
+ service,
103
+ error: err.message,
104
+ errorName: err.name,
105
+ stack: err.stack
106
+ });
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Collect Linodes (compute instances).
113
+ */
114
+ async *_collectLinodes() {
115
+ try {
116
+ const { getLinodes } = await import('@linode/api-v4/lib/linodes');
117
+
118
+ const response = await getLinodes();
119
+ const linodes = response.data || [];
120
+
121
+ for (const linode of linodes) {
122
+ yield {
123
+ provider: 'linode',
124
+ accountId: this._accountId,
125
+ region: linode.region,
126
+ service: 'linodes',
127
+ resourceType: 'linode.compute.instance',
128
+ resourceId: linode.id?.toString(),
129
+ name: linode.label,
130
+ tags: linode.tags || [],
131
+ configuration: this._sanitize(linode)
132
+ };
133
+ }
134
+
135
+ this.logger('info', `Collected ${linodes.length} Linode instances`);
136
+ } catch (err) {
137
+ this.logger('error', 'Failed to collect Linode instances', {
138
+ error: err.message,
139
+ stack: err.stack
140
+ });
141
+ throw err;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Collect Kubernetes (LKE) clusters.
147
+ */
148
+ async *_collectKubernetes() {
149
+ try {
150
+ const { getKubernetesClusters, getKubernetesClusterPools } = await import('@linode/api-v4/lib/kubernetes');
151
+
152
+ const response = await getKubernetesClusters();
153
+ const clusters = response.data || [];
154
+
155
+ for (const cluster of clusters) {
156
+ yield {
157
+ provider: 'linode',
158
+ accountId: this._accountId,
159
+ region: cluster.region,
160
+ service: 'kubernetes',
161
+ resourceType: 'linode.kubernetes.cluster',
162
+ resourceId: cluster.id?.toString(),
163
+ name: cluster.label,
164
+ tags: cluster.tags || [],
165
+ configuration: this._sanitize(cluster)
166
+ };
167
+
168
+ // Collect node pools for this cluster
169
+ try {
170
+ const poolsResponse = await getKubernetesClusterPools(cluster.id);
171
+ const pools = poolsResponse.data || [];
172
+
173
+ for (const pool of pools) {
174
+ yield {
175
+ provider: 'linode',
176
+ accountId: this._accountId,
177
+ region: cluster.region,
178
+ service: 'kubernetes',
179
+ resourceType: 'linode.kubernetes.nodepool',
180
+ resourceId: pool.id?.toString(),
181
+ name: `${cluster.label}-${pool.type}`,
182
+ tags: cluster.tags || [],
183
+ metadata: { clusterId: cluster.id, clusterLabel: cluster.label },
184
+ configuration: this._sanitize(pool)
185
+ };
186
+ }
187
+ } catch (poolErr) {
188
+ this.logger('warn', `Failed to collect node pools for cluster ${cluster.id}`, {
189
+ clusterId: cluster.id,
190
+ error: poolErr.message
191
+ });
192
+ }
193
+ }
194
+
195
+ this.logger('info', `Collected ${clusters.length} Linode Kubernetes clusters`);
196
+ } catch (err) {
197
+ this.logger('error', 'Failed to collect Linode Kubernetes', {
198
+ error: err.message,
199
+ stack: err.stack
200
+ });
201
+ throw err;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Collect Block Storage volumes.
207
+ */
208
+ async *_collectVolumes() {
209
+ try {
210
+ const { getVolumes } = await import('@linode/api-v4/lib/volumes');
211
+
212
+ const response = await getVolumes();
213
+ const volumes = response.data || [];
214
+
215
+ for (const volume of volumes) {
216
+ yield {
217
+ provider: 'linode',
218
+ accountId: this._accountId,
219
+ region: volume.region,
220
+ service: 'volumes',
221
+ resourceType: 'linode.volume',
222
+ resourceId: volume.id?.toString(),
223
+ name: volume.label,
224
+ tags: volume.tags || [],
225
+ configuration: this._sanitize(volume)
226
+ };
227
+ }
228
+
229
+ this.logger('info', `Collected ${volumes.length} Linode volumes`);
230
+ } catch (err) {
231
+ this.logger('error', 'Failed to collect Linode volumes', {
232
+ error: err.message,
233
+ stack: err.stack
234
+ });
235
+ throw err;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Collect NodeBalancers (load balancers).
241
+ */
242
+ async *_collectNodeBalancers() {
243
+ try {
244
+ const { getNodeBalancers } = await import('@linode/api-v4/lib/nodebalancers');
245
+
246
+ const response = await getNodeBalancers();
247
+ const nodebalancers = response.data || [];
248
+
249
+ for (const nb of nodebalancers) {
250
+ yield {
251
+ provider: 'linode',
252
+ accountId: this._accountId,
253
+ region: nb.region,
254
+ service: 'nodebalancers',
255
+ resourceType: 'linode.nodebalancer',
256
+ resourceId: nb.id?.toString(),
257
+ name: nb.label,
258
+ tags: nb.tags || [],
259
+ configuration: this._sanitize(nb)
260
+ };
261
+ }
262
+
263
+ this.logger('info', `Collected ${nodebalancers.length} Linode NodeBalancers`);
264
+ } catch (err) {
265
+ this.logger('error', 'Failed to collect Linode NodeBalancers', {
266
+ error: err.message,
267
+ stack: err.stack
268
+ });
269
+ throw err;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Collect Firewalls.
275
+ */
276
+ async *_collectFirewalls() {
277
+ try {
278
+ const { getFirewalls } = await import('@linode/api-v4/lib/firewalls');
279
+
280
+ const response = await getFirewalls();
281
+ const firewalls = response.data || [];
282
+
283
+ for (const firewall of firewalls) {
284
+ yield {
285
+ provider: 'linode',
286
+ accountId: this._accountId,
287
+ region: null, // Firewalls are global
288
+ service: 'firewalls',
289
+ resourceType: 'linode.firewall',
290
+ resourceId: firewall.id?.toString(),
291
+ name: firewall.label,
292
+ tags: firewall.tags || [],
293
+ configuration: this._sanitize(firewall)
294
+ };
295
+ }
296
+
297
+ this.logger('info', `Collected ${firewalls.length} Linode firewalls`);
298
+ } catch (err) {
299
+ this.logger('error', 'Failed to collect Linode firewalls', {
300
+ error: err.message,
301
+ stack: err.stack
302
+ });
303
+ throw err;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Collect VLANs.
309
+ */
310
+ async *_collectVLANs() {
311
+ try {
312
+ const { getVLANs } = await import('@linode/api-v4/lib/vlans');
313
+
314
+ const response = await getVLANs();
315
+ const vlans = response.data || [];
316
+
317
+ for (const vlan of vlans) {
318
+ yield {
319
+ provider: 'linode',
320
+ accountId: this._accountId,
321
+ region: vlan.region,
322
+ service: 'vlans',
323
+ resourceType: 'linode.vlan',
324
+ resourceId: vlan.label, // VLANs use label as ID
325
+ name: vlan.label,
326
+ tags: [],
327
+ configuration: this._sanitize(vlan)
328
+ };
329
+ }
330
+
331
+ this.logger('info', `Collected ${vlans.length} Linode VLANs`);
332
+ } catch (err) {
333
+ this.logger('error', 'Failed to collect Linode VLANs', {
334
+ error: err.message,
335
+ stack: err.stack
336
+ });
337
+ throw err;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Collect DNS domains and records.
343
+ */
344
+ async *_collectDomains() {
345
+ try {
346
+ const { getDomains, getDomainRecords } = await import('@linode/api-v4/lib/domains');
347
+
348
+ const response = await getDomains();
349
+ const domains = response.data || [];
350
+
351
+ for (const domain of domains) {
352
+ yield {
353
+ provider: 'linode',
354
+ accountId: this._accountId,
355
+ region: null, // DNS is global
356
+ service: 'domains',
357
+ resourceType: 'linode.dns.domain',
358
+ resourceId: domain.id?.toString(),
359
+ name: domain.domain,
360
+ tags: domain.tags || [],
361
+ configuration: this._sanitize(domain)
362
+ };
363
+
364
+ // Collect DNS records for this domain
365
+ try {
366
+ const recordsResponse = await getDomainRecords(domain.id);
367
+ const records = recordsResponse.data || [];
368
+
369
+ for (const record of records) {
370
+ yield {
371
+ provider: 'linode',
372
+ accountId: this._accountId,
373
+ region: null,
374
+ service: 'domains',
375
+ resourceType: 'linode.dns.record',
376
+ resourceId: `${domain.id}/${record.id}`,
377
+ name: `${record.name}.${domain.domain}`,
378
+ tags: [],
379
+ metadata: { domainId: domain.id, domain: domain.domain },
380
+ configuration: this._sanitize(record)
381
+ };
382
+ }
383
+ } catch (recordErr) {
384
+ this.logger('warn', `Failed to collect DNS records for domain ${domain.domain}`, {
385
+ domainId: domain.id,
386
+ error: recordErr.message
387
+ });
388
+ }
389
+ }
390
+
391
+ this.logger('info', `Collected ${domains.length} Linode DNS domains`);
392
+ } catch (err) {
393
+ this.logger('error', 'Failed to collect Linode domains', {
394
+ error: err.message,
395
+ stack: err.stack
396
+ });
397
+ throw err;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Collect custom Images.
403
+ */
404
+ async *_collectImages() {
405
+ try {
406
+ const { getImages } = await import('@linode/api-v4/lib/images');
407
+
408
+ const response = await getImages();
409
+ const images = response.data || [];
410
+
411
+ // Filter to only custom images (not official Linode images)
412
+ const customImages = images.filter(img => img.is_public === false);
413
+
414
+ for (const image of customImages) {
415
+ yield {
416
+ provider: 'linode',
417
+ accountId: this._accountId,
418
+ region: image.region || null,
419
+ service: 'images',
420
+ resourceType: 'linode.image',
421
+ resourceId: image.id,
422
+ name: image.label,
423
+ tags: [],
424
+ configuration: this._sanitize(image)
425
+ };
426
+ }
427
+
428
+ this.logger('info', `Collected ${customImages.length} custom Linode images`);
429
+ } catch (err) {
430
+ this.logger('error', 'Failed to collect Linode images', {
431
+ error: err.message,
432
+ stack: err.stack
433
+ });
434
+ throw err;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Collect Object Storage buckets.
440
+ */
441
+ async *_collectObjectStorage() {
442
+ try {
443
+ const { getObjectStorageBuckets } = await import('@linode/api-v4/lib/object-storage');
444
+
445
+ const response = await getObjectStorageBuckets();
446
+ const buckets = response.data || [];
447
+
448
+ for (const bucket of buckets) {
449
+ yield {
450
+ provider: 'linode',
451
+ accountId: this._accountId,
452
+ region: bucket.region,
453
+ service: 'objectstorage',
454
+ resourceType: 'linode.objectstorage.bucket',
455
+ resourceId: `${bucket.cluster}/${bucket.label}`,
456
+ name: bucket.label,
457
+ tags: [],
458
+ metadata: { cluster: bucket.cluster },
459
+ configuration: this._sanitize(bucket)
460
+ };
461
+ }
462
+
463
+ this.logger('info', `Collected ${buckets.length} Linode object storage buckets`);
464
+ } catch (err) {
465
+ this.logger('error', 'Failed to collect Linode object storage', {
466
+ error: err.message,
467
+ stack: err.stack
468
+ });
469
+ throw err;
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Collect Managed Databases (MySQL, PostgreSQL, MongoDB).
475
+ */
476
+ async *_collectDatabases() {
477
+ try {
478
+ const { getDatabases } = await import('@linode/api-v4/lib/databases');
479
+
480
+ const response = await getDatabases();
481
+ const databases = response.data || [];
482
+
483
+ for (const db of databases) {
484
+ yield {
485
+ provider: 'linode',
486
+ accountId: this._accountId,
487
+ region: db.region,
488
+ service: 'databases',
489
+ resourceType: 'linode.database',
490
+ resourceId: db.id?.toString(),
491
+ name: db.label,
492
+ tags: [],
493
+ metadata: {
494
+ engine: db.engine,
495
+ version: db.version,
496
+ status: db.status
497
+ },
498
+ configuration: this._sanitize(db)
499
+ };
500
+ }
501
+
502
+ this.logger('info', `Collected ${databases.length} Linode databases`);
503
+ } catch (err) {
504
+ this.logger('error', 'Failed to collect Linode databases', {
505
+ error: err.message,
506
+ stack: err.stack
507
+ });
508
+ throw err;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Collect StackScripts (deployment scripts).
514
+ */
515
+ async *_collectStackScripts() {
516
+ try {
517
+ const { getStackScripts } = await import('@linode/api-v4/lib/stackscripts');
518
+
519
+ // Only collect user's own StackScripts (not public ones)
520
+ const response = await getStackScripts({ mine: true });
521
+ const stackScripts = response.data || [];
522
+
523
+ for (const script of stackScripts) {
524
+ yield {
525
+ provider: 'linode',
526
+ accountId: this._accountId,
527
+ region: null, // StackScripts are global
528
+ service: 'stackscripts',
529
+ resourceType: 'linode.stackscript',
530
+ resourceId: script.id?.toString(),
531
+ name: script.label,
532
+ tags: [],
533
+ metadata: {
534
+ isPublic: script.is_public,
535
+ deploymentsTotal: script.deployments_total
536
+ },
537
+ configuration: this._sanitize(script)
538
+ };
539
+ }
540
+
541
+ this.logger('info', `Collected ${stackScripts.length} Linode StackScripts`);
542
+ } catch (err) {
543
+ this.logger('error', 'Failed to collect Linode StackScripts', {
544
+ error: err.message,
545
+ stack: err.stack
546
+ });
547
+ throw err;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Collect Placement Groups (anti-affinity groups).
553
+ */
554
+ async *_collectPlacementGroups() {
555
+ try {
556
+ const { getPlacementGroups } = await import('@linode/api-v4/lib/placement-groups');
557
+
558
+ const response = await getPlacementGroups();
559
+ const placementGroups = response.data || [];
560
+
561
+ for (const pg of placementGroups) {
562
+ yield {
563
+ provider: 'linode',
564
+ accountId: this._accountId,
565
+ region: pg.region,
566
+ service: 'placementgroups',
567
+ resourceType: 'linode.placementgroup',
568
+ resourceId: pg.id?.toString(),
569
+ name: pg.label,
570
+ tags: [],
571
+ metadata: {
572
+ placementGroupType: pg.placement_group_type,
573
+ placementGroupPolicy: pg.placement_group_policy
574
+ },
575
+ configuration: this._sanitize(pg)
576
+ };
577
+ }
578
+
579
+ this.logger('info', `Collected ${placementGroups.length} Linode placement groups`);
580
+ } catch (err) {
581
+ this.logger('error', 'Failed to collect Linode placement groups', {
582
+ error: err.message,
583
+ stack: err.stack
584
+ });
585
+ throw err;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Sanitize configuration by removing sensitive data.
591
+ */
592
+ _sanitize(config) {
593
+ if (!config || typeof config !== 'object') return config;
594
+
595
+ const sanitized = { ...config };
596
+ const sensitiveFields = [
597
+ 'root_pass',
598
+ 'password',
599
+ 'token',
600
+ 'secret',
601
+ 'api_key',
602
+ 'private_key',
603
+ 'public_key'
604
+ ];
605
+
606
+ for (const field of sensitiveFields) {
607
+ if (field in sanitized) {
608
+ sanitized[field] = '***REDACTED***';
609
+ }
610
+ }
611
+
612
+ return sanitized;
613
+ }
614
+ }