@fenwave/agent 1.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.
@@ -0,0 +1,1100 @@
1
+ import axios from "axios";
2
+ import chalk from "chalk";
3
+ import {
4
+ ECRClient,
5
+ DescribeRepositoriesCommand,
6
+ DescribeImagesCommand,
7
+ } from "@aws-sdk/client-ecr";
8
+ import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
9
+ import { GoogleAuth } from "google-auth-library";
10
+ import { formatSize, formatCreatedTime } from "../helper-functions.js";
11
+ import registryStore from "../store/registryStore.js";
12
+
13
+ async function handleRegistryAction(ws, action, payload) {
14
+ switch (action) {
15
+ case "fetchRegistries":
16
+ return await handleFetchRegistries(ws, payload);
17
+ case "connectRegistry":
18
+ return await handleConnectRegistry(ws, payload);
19
+ case "disconnectRegistry":
20
+ return await handleDisconnectRegistry(ws, payload);
21
+ case "renameRegistry":
22
+ return await handleRenameRegistry(ws, payload);
23
+ case "setActiveRegistry":
24
+ return await handleSetActiveRegistry(ws, payload);
25
+ case "fetchRegistryImages":
26
+ return await handleFetchRegistryImages(ws, payload);
27
+ default:
28
+ throw new Error(`Unknown registry action: ${action}`);
29
+ }
30
+ }
31
+
32
+ async function handleFetchRegistries(ws, payload = {}) {
33
+ try {
34
+ // Get all registries from persistent store
35
+ const registries = await registryStore.getAllRegistries();
36
+
37
+ ws.send(
38
+ JSON.stringify({
39
+ type: "registries",
40
+ registries,
41
+ requestId: payload.requestId,
42
+ })
43
+ );
44
+ } catch (error) {
45
+ console.error("Error fetching registries:", error);
46
+ ws.send(
47
+ JSON.stringify({
48
+ type: "error",
49
+ error: "Failed to fetch registries: " + error.message,
50
+ requestId: payload.requestId,
51
+ })
52
+ );
53
+ }
54
+ }
55
+
56
+ async function handleConnectRegistry(ws, payload) {
57
+ try {
58
+ const {
59
+ name,
60
+ url,
61
+ type,
62
+ username,
63
+ password,
64
+ accessKeyId,
65
+ secretAccessKey,
66
+ region,
67
+ sessionToken,
68
+ serviceAccountJson,
69
+ requestId,
70
+ } = payload;
71
+
72
+ // Basic URL validation
73
+ try {
74
+ new URL(url);
75
+ } catch (error) {
76
+ throw new Error(`Invalid registry URL: ${error.message}`);
77
+ }
78
+
79
+ let authenticationValid = false;
80
+ let errorMessage = "";
81
+ let extractedUsername = username;
82
+
83
+ try {
84
+ switch (type) {
85
+ case "docker-hub":
86
+ authenticationValid = await testDockerHubCredentials(
87
+ username,
88
+ password
89
+ );
90
+ break;
91
+
92
+ case "ecr":
93
+ const ecrResult = await testECRCredentials(
94
+ accessKeyId,
95
+ secretAccessKey,
96
+ region,
97
+ sessionToken
98
+ );
99
+ authenticationValid = ecrResult.success;
100
+ extractedUsername = ecrResult.username;
101
+ break;
102
+
103
+ case "gcr":
104
+ authenticationValid = await testGCRCredentials(
105
+ serviceAccountJson,
106
+ url
107
+ );
108
+ break;
109
+
110
+ case "acr":
111
+ authenticationValid = await testACRCredentials(
112
+ username,
113
+ password,
114
+ url
115
+ );
116
+ break;
117
+
118
+ case "custom":
119
+ authenticationValid = await testCustomRegistryCredentials(
120
+ username,
121
+ password,
122
+ url
123
+ );
124
+ break;
125
+
126
+ default:
127
+ throw new Error(`Unsupported registry type: ${type}`);
128
+ }
129
+ } catch (authError) {
130
+ authenticationValid = false;
131
+ errorMessage = authError.message;
132
+ }
133
+
134
+ if (!authenticationValid) {
135
+ throw new Error(
136
+ errorMessage || "Authentication failed: Invalid credentials"
137
+ );
138
+ }
139
+
140
+ // Generate a unique ID for the registry
141
+ const registryId = `reg-${Date.now()}`;
142
+
143
+ // Create registry object with proper credential storage and metadata
144
+ const registry = {
145
+ id: registryId,
146
+ name,
147
+ url,
148
+ type,
149
+ connected: true,
150
+ connectedAt: new Date().toISOString(),
151
+ // Store metadata at top level for API exposure
152
+ ...(type === "ecr" && {
153
+ username: extractedUsername,
154
+ region,
155
+ }),
156
+ ...(type === "gcr" &&
157
+ serviceAccountJson &&
158
+ (() => {
159
+ try {
160
+ const serviceAccount = JSON.parse(serviceAccountJson);
161
+ return { projectId: serviceAccount.project_id };
162
+ } catch (error) {
163
+ console.error("Error parsing service account JSON:", error);
164
+ return {};
165
+ }
166
+ })()),
167
+ ...(username && { username }),
168
+ credentials: {
169
+ // Store credentials based on registry type
170
+ ...(type === "ecr" && {
171
+ accessKeyId,
172
+ secretAccessKey,
173
+ sessionToken,
174
+ }),
175
+ ...(type === "gcr" && { serviceAccountJson }),
176
+ ...(type === "docker-hub" && { username, password }),
177
+ ...(type === "acr" && { username, password }),
178
+ ...(type === "custom" && { username, password }),
179
+ },
180
+ };
181
+
182
+ // Store the registry persistently
183
+ await registryStore.upsertRegistry(registry);
184
+
185
+ console.log(`✅ Successfully connected to registry: ${name}`);
186
+
187
+ // Return registry with safe metadata (credentials excluded)
188
+ const { credentials, ...safeRegistry } = registry;
189
+
190
+ ws.send(
191
+ JSON.stringify({
192
+ type: "registryConnected",
193
+ registry: safeRegistry,
194
+ requestId,
195
+ })
196
+ );
197
+ } catch (error) {
198
+ console.error(chalk.red("❌ Error connecting to registry:", error));
199
+ ws.send(
200
+ JSON.stringify({
201
+ type: "error",
202
+ error: "Failed to connect to registry: " + error.message,
203
+ requestId: payload.requestId,
204
+ })
205
+ );
206
+ }
207
+ }
208
+
209
+ // Test Docker Hub credentials
210
+ async function testDockerHubCredentials(username, password) {
211
+ const baseUrl = "https://hub.docker.com/v2";
212
+ try {
213
+ const tokenResponse = await axios.post(
214
+ `${baseUrl}/users/login/`,
215
+ {
216
+ username: username,
217
+ password: password,
218
+ },
219
+ {
220
+ headers: {
221
+ "Content-Type": "application/json",
222
+ },
223
+ timeout: 15000,
224
+ }
225
+ );
226
+
227
+ const token = tokenResponse.data?.token;
228
+ if (!token) {
229
+ throw new Error("No authentication token received");
230
+ }
231
+
232
+ // Fetch user repositories
233
+ const testResponse = await axios.get(
234
+ `${baseUrl}/repositories/${username}/`,
235
+ {
236
+ headers: {
237
+ Authorization: `JWT ${token}`,
238
+ },
239
+ params: {
240
+ page_size: 1,
241
+ },
242
+ timeout: 10000,
243
+ }
244
+ );
245
+
246
+ if (testResponse.status !== 200) {
247
+ throw new Error(`Unexpected response status: ${testResponse.status}`);
248
+ }
249
+
250
+ return true;
251
+ } catch (error) {
252
+ console.error(
253
+ chalk.red(`❌ Docker Hub authentication failed:`, error.message)
254
+ );
255
+
256
+ if (error.response?.status === 401) {
257
+ throw new Error(
258
+ error.response?.data?.detail || "Invalid username or password/token"
259
+ );
260
+ } else if (error.response?.status === 429) {
261
+ throw new Error("Rate limited by Docker Hub API");
262
+ } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
263
+ throw new Error("Cannot connect to Docker Hub");
264
+ } else {
265
+ throw new Error(`Authentication test failed: ${error.message}`);
266
+ }
267
+ }
268
+ }
269
+
270
+ // Test ECR registry credentials
271
+ async function testECRCredentials(
272
+ accessKeyId,
273
+ secretAccessKey,
274
+ region,
275
+ sessionToken
276
+ ) {
277
+ try {
278
+ if (!region || !region.match(/^[a-z0-9-]+$/)) {
279
+ throw new Error("Invalid AWS region format");
280
+ }
281
+
282
+ const credentials = {
283
+ accessKeyId,
284
+ secretAccessKey,
285
+ sessionToken,
286
+ };
287
+
288
+ const stsClient = new STSClient({ region, credentials });
289
+ const identity = await stsClient.send(new GetCallerIdentityCommand({}));
290
+
291
+ let username = "Unknown";
292
+ if (identity.Arn) {
293
+ const arnParts = identity.Arn.split("/");
294
+ if (arnParts.length > 0) {
295
+ username = arnParts[arnParts.length - 1];
296
+ }
297
+ }
298
+
299
+ const ecrClient = new ECRClient({ region, credentials });
300
+ const command = new DescribeRepositoriesCommand({ maxResults: 1 });
301
+
302
+ await ecrClient.send(command);
303
+
304
+ return { success: true, username };
305
+ } catch (error) {
306
+ console.error(
307
+ chalk.red(`❌ AWS authentication failed:`, error.name, ":", error.message)
308
+ );
309
+
310
+ switch (error.name) {
311
+ case "ExpiredTokenException":
312
+ throw new Error(
313
+ "AWS session has expired. Please use fresh credentials."
314
+ );
315
+ case "AccessDeniedException":
316
+ case "UnrecognizedClientException":
317
+ throw new Error("Invalid AWS credentials or insufficient permissions");
318
+ case "InvalidSignatureException":
319
+ throw new Error("Invalid AWS Secret Access Key");
320
+ case "ValidationException":
321
+ throw new Error("Invalid AWS region or parameter");
322
+ case "CredentialsProviderError":
323
+ throw new Error("AWS Access Key ID not found");
324
+ case "NetworkingError":
325
+ throw new Error("Cannot connect to AWS service");
326
+ case "TokenRefreshRequired":
327
+ throw new Error(
328
+ "AWS credentials need to be refreshed. Please use fresh credentials."
329
+ );
330
+ default:
331
+ throw new Error(`Authentication test failed: ${error.message}`);
332
+ }
333
+ }
334
+ }
335
+
336
+ // Test GCR registry credentials
337
+ async function testGCRCredentials(serviceAccountJson) {
338
+ try {
339
+ let serviceAccountKey;
340
+ try {
341
+ serviceAccountKey = JSON.parse(serviceAccountJson);
342
+ } catch (parseError) {
343
+ throw new Error("Invalid service account JSON format");
344
+ }
345
+
346
+ if (
347
+ !serviceAccountKey.type ||
348
+ serviceAccountKey.type !== "service_account"
349
+ ) {
350
+ throw new Error("Invalid service account type");
351
+ }
352
+ if (!serviceAccountKey.project_id) {
353
+ throw new Error("Missing project_id in service account");
354
+ }
355
+ if (!serviceAccountKey.private_key || !serviceAccountKey.client_email) {
356
+ throw new Error("Missing required credentials in service account");
357
+ }
358
+
359
+ const auth = new GoogleAuth({
360
+ credentials: serviceAccountKey,
361
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"],
362
+ });
363
+
364
+ const authClient = await auth.getClient();
365
+ const accessToken = await authClient.getAccessToken();
366
+
367
+ if (!accessToken.token) {
368
+ throw new Error("Failed to obtain access token");
369
+ }
370
+
371
+ const projectId = serviceAccountKey.project_id;
372
+ const registryUrl = `https://gcr.io/v2/${projectId}/tags/list`;
373
+
374
+ const response = await axios.get(registryUrl, {
375
+ headers: {
376
+ Authorization: `Bearer ${accessToken.token}`,
377
+ Accept: "application/json",
378
+ },
379
+ timeout: 10000,
380
+ });
381
+
382
+ if (response.status === 200) {
383
+ return true;
384
+ } else {
385
+ throw new Error(`Unexpected response status: ${response.status}`);
386
+ }
387
+ } catch (error) {
388
+ console.error(chalk.red(`❌ GCR authentication failed:`, error.message));
389
+
390
+ if (error.response?.status === 401) {
391
+ throw new Error("Invalid service account credentials");
392
+ } else if (error.response?.status === 403) {
393
+ throw new Error(
394
+ "Service account lacks required permissions for Container Registry"
395
+ );
396
+ } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
397
+ throw new Error("Cannot connect to Google Container Registry");
398
+ } else if (error.message.includes("JSON")) {
399
+ throw new Error("Invalid service account JSON format");
400
+ } else {
401
+ throw new Error(`GCR authentication test failed: ${error.message}`);
402
+ }
403
+ }
404
+ }
405
+
406
+ // Test ACR registry credentials
407
+ async function testACRCredentials(username, password, url) {
408
+ try {
409
+ const registryName = url.replace("https://", "").replace(".azurecr.io", "");
410
+
411
+ const catalogUrl = `${url}/v2/_catalog`;
412
+
413
+ const response = await axios.get(catalogUrl, {
414
+ headers: {
415
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
416
+ "base64"
417
+ )}`,
418
+ },
419
+ timeout: 10000,
420
+ });
421
+
422
+ if (response.status === 200) {
423
+ return true;
424
+ } else {
425
+ throw new Error(`Unexpected response status: ${response.status}`);
426
+ }
427
+ } catch (error) {
428
+ console.error(chalk.red(`❌ ACR authentication failed:`, error.message));
429
+
430
+ if (error.response?.status === 401) {
431
+ throw new Error("Invalid username or password/token");
432
+ } else if (error.response?.status === 404) {
433
+ throw new Error("Registry not found or API not supported");
434
+ } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
435
+ throw new Error("Cannot connect to Azure Container Registry");
436
+ } else {
437
+ throw new Error(`ACR authentication test failed: ${error.message}`);
438
+ }
439
+ }
440
+ }
441
+
442
+ // Test custom registry credentials
443
+ async function testCustomRegistryCredentials(username, password, url) {
444
+ try {
445
+ const response = await axios.get(`${url}/v2/_catalog`, {
446
+ headers: {
447
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(
448
+ "base64"
449
+ )}`,
450
+ },
451
+ timeout: 10000,
452
+ });
453
+
454
+ if (response.status === 200) {
455
+ return true;
456
+ } else {
457
+ throw new Error(`Unexpected response status: ${response.status}`);
458
+ }
459
+ } catch (error) {
460
+ console.error(
461
+ chalk.red(`❌ Custom registry authentication failed:`, error.message)
462
+ );
463
+
464
+ if (error.response?.status === 401) {
465
+ throw new Error("Invalid username or password/token");
466
+ } else if (error.response?.status === 404) {
467
+ throw new Error("Registry not found or API not supported");
468
+ } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
469
+ throw new Error("Cannot connect to registry");
470
+ } else {
471
+ throw new Error(`Authentication test failed: ${error.message}`);
472
+ }
473
+ }
474
+ }
475
+
476
+ async function handleDisconnectRegistry(ws, payload) {
477
+ try {
478
+ const { id, requestId } = payload;
479
+
480
+ if (!id) {
481
+ throw new Error("Registry ID is required");
482
+ }
483
+
484
+ // Get registry from store first to check if it exists
485
+ const registry = await registryStore.getRegistryWithCredentials(id);
486
+ if (!registry) {
487
+ throw new Error("Registry not found");
488
+ }
489
+
490
+ // Remove from persistent store
491
+ await registryStore.removeRegistry(id);
492
+
493
+ console.log(`✅ Successfully disconnected from registry: ${registry.name}`);
494
+
495
+ ws.send(
496
+ JSON.stringify({
497
+ type: "registryDisconnected",
498
+ id,
499
+ success: true,
500
+ requestId,
501
+ })
502
+ );
503
+ } catch (error) {
504
+ console.error("Error disconnecting registry:", error);
505
+ ws.send(
506
+ JSON.stringify({
507
+ type: "error",
508
+ error: "Failed to disconnect registry: " + error.message,
509
+ requestId: payload.requestId,
510
+ })
511
+ );
512
+ }
513
+ }
514
+
515
+ async function handleRenameRegistry(ws, payload) {
516
+ try {
517
+ const { id, newName, requestId } = payload;
518
+
519
+ if (!id) {
520
+ throw new Error("Registry ID is required");
521
+ }
522
+
523
+ if (!newName || newName.trim() === "") {
524
+ throw new Error("New name is required");
525
+ }
526
+
527
+ // Get registry from store first to check if it exists
528
+ const registry = await registryStore.getRegistryWithCredentials(id);
529
+ if (!registry) {
530
+ throw new Error("Registry not found");
531
+ }
532
+
533
+ const oldName = registry.name;
534
+
535
+ // Update the registry with the new name
536
+ const updatedRegistry = {
537
+ ...registry,
538
+ name: newName.trim(),
539
+ };
540
+
541
+ // Save the updated registry
542
+ await registryStore.upsertRegistry(updatedRegistry);
543
+
544
+ console.log(
545
+ `✅ Successfully renamed registry from: ${oldName} to: ${newName.trim()}`
546
+ );
547
+
548
+ // Return safe registry data (without credentials)
549
+ const { credentials, ...safeRegistry } = updatedRegistry;
550
+
551
+ ws.send(
552
+ JSON.stringify({
553
+ type: "registryRenamed",
554
+ registry: safeRegistry,
555
+ requestId,
556
+ })
557
+ );
558
+ } catch (error) {
559
+ console.error("Error renaming registry:", error);
560
+ ws.send(
561
+ JSON.stringify({
562
+ type: "error",
563
+ error: "Failed to rename registry: " + error.message,
564
+ requestId: payload.requestId,
565
+ })
566
+ );
567
+ }
568
+ }
569
+
570
+ async function handleSetActiveRegistry(ws, payload) {
571
+ try {
572
+ const { id, requestId } = payload;
573
+
574
+ if (!id) {
575
+ throw new Error("Registry ID is required");
576
+ }
577
+
578
+ // Set the active registry
579
+ const activeRegistry = await registryStore.setActiveRegistry(id);
580
+
581
+ if (!activeRegistry) {
582
+ throw new Error("Registry not found");
583
+ }
584
+
585
+ // Return safe registry data (without credentials)
586
+ const { credentials, ...safeRegistry } = activeRegistry;
587
+
588
+ ws.send(
589
+ JSON.stringify({
590
+ type: "activeRegistrySet",
591
+ registry: safeRegistry,
592
+ requestId,
593
+ })
594
+ );
595
+ } catch (error) {
596
+ console.error("Error setting active registry:", error);
597
+ ws.send(
598
+ JSON.stringify({
599
+ type: "error",
600
+ error: "Failed to set active registry: " + error.message,
601
+ requestId: payload.requestId,
602
+ })
603
+ );
604
+ }
605
+ }
606
+
607
+ async function handleFetchRegistryImages(ws, payload) {
608
+ try {
609
+ const { registryId, requestId } = payload;
610
+
611
+ // Get registry from persistent store
612
+ const registry = await registryStore.getRegistryWithCredentials(registryId);
613
+ if (!registry) {
614
+ throw new Error("Registry not found");
615
+ }
616
+
617
+ let formattedImages = [];
618
+
619
+ try {
620
+ switch (registry.type) {
621
+ case "docker-hub":
622
+ formattedImages = await fetchDockerHubImages(registry);
623
+ break;
624
+ case "ecr":
625
+ formattedImages = await fetchECRImages(registry);
626
+ break;
627
+ case "gcr":
628
+ formattedImages = await fetchGCRImages(registry);
629
+ break;
630
+ case "acr":
631
+ formattedImages = await fetchACRImages(registry);
632
+ break;
633
+ case "custom":
634
+ formattedImages = await fetchCustomRegistryImages(registry);
635
+ break;
636
+ default:
637
+ throw new Error(`Unsupported registry type: ${registry.type}`);
638
+ }
639
+ } catch (fetchError) {
640
+ console.error(
641
+ `Error fetching images from registry ${registry.name}:`,
642
+ fetchError.message
643
+ );
644
+
645
+ ws.send(
646
+ JSON.stringify({
647
+ type: "error",
648
+ error: `Failed to fetch images from registry ${registry.name}: ${fetchError.message}`,
649
+ requestId,
650
+ })
651
+ );
652
+ return;
653
+ }
654
+
655
+ ws.send(
656
+ JSON.stringify({
657
+ type: "registryImages",
658
+ images: formattedImages,
659
+ requestId,
660
+ })
661
+ );
662
+ } catch (error) {
663
+ console.error("Error fetching registry images:", error);
664
+ ws.send(
665
+ JSON.stringify({
666
+ type: "error",
667
+ error: "Failed to fetch registry images: " + error.message,
668
+ requestId: payload.requestId,
669
+ })
670
+ );
671
+ }
672
+ }
673
+
674
+ // Helper function to fetch images from Docker Hub
675
+ async function fetchDockerHubImages(registry) {
676
+ const baseUrl = "https://hub.docker.com/v2";
677
+ try {
678
+ const username = registry.credentials.username;
679
+ const password = registry.credentials.password;
680
+
681
+ const tokenResponse = await axios.post(
682
+ `${baseUrl}/users/login/`,
683
+ {
684
+ username: username,
685
+ password: password,
686
+ },
687
+ {
688
+ headers: {
689
+ "Content-Type": "application/json",
690
+ },
691
+ timeout: 15000,
692
+ }
693
+ );
694
+
695
+ const token = tokenResponse.data?.token;
696
+ if (!token) {
697
+ throw new Error(
698
+ "Failed to get authentication token - invalid credentials"
699
+ );
700
+ }
701
+
702
+ const reposResponse = await axios.get(
703
+ `${baseUrl}/repositories/${username}/`,
704
+ {
705
+ headers: {
706
+ Authorization: `JWT ${token}`,
707
+ },
708
+ params: {
709
+ page_size: 100,
710
+ },
711
+ timeout: 15000,
712
+ }
713
+ );
714
+
715
+ const repositories = reposResponse.data.results || [];
716
+ if (repositories.length === 0) {
717
+ console.warn(`⚠️ No repositories found for user: ${username}`);
718
+ return [];
719
+ }
720
+
721
+ const imagePromises = repositories.map(async (repo) => {
722
+ try {
723
+ const tagsResponse = await axios.get(
724
+ `${baseUrl}/repositories/${username}/${repo.name}/tags/`,
725
+ {
726
+ headers: {
727
+ Authorization: `JWT ${token}`,
728
+ },
729
+ params: {
730
+ page_size: 100,
731
+ },
732
+ timeout: 10000,
733
+ }
734
+ );
735
+
736
+ const tags = tagsResponse.data.results || [];
737
+
738
+ return tags.map((tag) => ({
739
+ id: `dh-${repo.name}-${tag.name}`,
740
+ name: `${username}/${repo.name}`,
741
+ tag: tag.name,
742
+ size: tag.full_size ? formatSize(tag.full_size) : "Unknown",
743
+ created: tag.last_updated
744
+ ? formatCreatedTime(new Date(tag.last_updated).getTime() / 1000)
745
+ : "Unknown",
746
+ registry: registry.id,
747
+ isPrivate: repo.is_private || false,
748
+ starCount: repo.star_count || 0,
749
+ pullCount: repo.pull_count || 0,
750
+ architecture: tag.images?.[0]?.architecture || "amd64",
751
+ os: tag.images?.[0]?.os || "linux",
752
+ }));
753
+ } catch (tagError) {
754
+ console.error(
755
+ chalk.red(
756
+ `❌ Error fetching tags for repository ${repo.name}:`,
757
+ tagError.message
758
+ )
759
+ );
760
+ return [];
761
+ }
762
+ });
763
+
764
+ const imageArrays = await Promise.all(imagePromises);
765
+ const allImages = imageArrays.flat().filter((image) => image);
766
+
767
+ return allImages;
768
+ } catch (error) {
769
+ console.error(chalk.red("❌ Error fetching Docker Hub images:", error));
770
+
771
+ if (error.response?.status === 401) {
772
+ throw new Error("Authentication failed: Invalid credentials");
773
+ } else if (error.response?.status === 429) {
774
+ throw new Error("Rate limited: Too many requests to Docker Hub API");
775
+ } else if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
776
+ throw new Error("Cannot connect to Docker Hub");
777
+ } else {
778
+ throw new Error(`Docker Hub API error: ${error.message}`);
779
+ }
780
+ }
781
+ }
782
+
783
+ // Helper function to fetch images from AWS ECR
784
+ async function fetchECRImages(registry) {
785
+ try {
786
+ const credentials = {
787
+ accessKeyId: registry.credentials.accessKeyId,
788
+ secretAccessKey: registry.credentials.secretAccessKey,
789
+ sessionToken: registry.credentials.sessionToken,
790
+ };
791
+
792
+ // Remove undefined values
793
+ Object.keys(credentials).forEach((key) => {
794
+ if (credentials[key] === undefined) {
795
+ delete credentials[key];
796
+ }
797
+ });
798
+
799
+ const ecr = new ECRClient({
800
+ region: registry.region,
801
+ credentials,
802
+ });
803
+
804
+ const reposResult = await ecr.send(
805
+ new DescribeRepositoriesCommand({
806
+ maxResults: 100,
807
+ })
808
+ );
809
+
810
+ const repositories = reposResult.repositories || [];
811
+
812
+ if (repositories.length === 0) {
813
+ return [];
814
+ }
815
+
816
+ const imagePromises = repositories.map(async (repo) => {
817
+ try {
818
+ const imagesResult = await ecr.send(
819
+ new DescribeImagesCommand({
820
+ repositoryName: repo.repositoryName,
821
+ maxResults: 100,
822
+ })
823
+ );
824
+
825
+ const images = imagesResult.imageDetails || [];
826
+
827
+ return images.map((image) => {
828
+ const tags = image.imageTags || ["<untagged>"];
829
+ const primaryTag = tags[0];
830
+
831
+ return {
832
+ id: `ecr-${repo.repositoryName}-${primaryTag}`,
833
+ name: `${registry.url.replace("https://", "")}/${
834
+ repo.repositoryName
835
+ }`,
836
+ tag: primaryTag,
837
+ size: image.imageSizeInBytes
838
+ ? formatSize(image.imageSizeInBytes)
839
+ : "Unknown",
840
+ created: image.imagePushedAt
841
+ ? formatCreatedTime(image.imagePushedAt.getTime() / 1000)
842
+ : "Unknown",
843
+ registry: registry.id,
844
+ isPrivate: true,
845
+ pullCount: 0,
846
+ starCount: 0,
847
+ architecture: "amd64",
848
+ os: "linux",
849
+ };
850
+ });
851
+ } catch (imageError) {
852
+ console.error(
853
+ chalk.red(
854
+ `❌ Error fetching images for repository ${repo.repositoryName}:`,
855
+ imageError.message
856
+ )
857
+ );
858
+ return [];
859
+ }
860
+ });
861
+
862
+ const imageArrays = await Promise.all(imagePromises);
863
+ const allImages = imageArrays.flat().filter((image) => image);
864
+
865
+ return allImages;
866
+ } catch (error) {
867
+ console.error(chalk.red("❌ Error fetching ECR images:", error));
868
+
869
+ // Handle expired token specifically
870
+ if (error.code === "ExpiredTokenException") {
871
+ throw new Error(
872
+ "AWS session has expired. Please reconnect your ECR registry with fresh credentials."
873
+ );
874
+ } else if (
875
+ error.code === "UnauthorizedOperation" ||
876
+ error.code === "AccessDenied"
877
+ ) {
878
+ throw new Error("Insufficient permissions to access ECR repositories");
879
+ } else if (error.code === "InvalidUserID.NotFound") {
880
+ throw new Error("Invalid AWS credentials");
881
+ } else if (error.code === "TokenRefreshRequired") {
882
+ throw new Error(
883
+ "AWS credentials need to be refreshed. Please reconnect your ECR registry."
884
+ );
885
+ } else {
886
+ throw new Error(`ECR API error: ${error.message}`);
887
+ }
888
+ }
889
+ }
890
+
891
+ //! TO BE ENHANCED IN THE FUTURE
892
+ // Helper function to fetch images from Google GCR
893
+ async function fetchGCRImages(registry) {
894
+ try {
895
+ const serviceAccountKey = JSON.parse(
896
+ registry.credentials.serviceAccountJson
897
+ );
898
+ const projectId = serviceAccountKey.project_id;
899
+
900
+ const auth = new GoogleAuth({
901
+ credentials: serviceAccountKey,
902
+ scopes: ["https://www.googleapis.com/auth/cloud-platform"],
903
+ });
904
+
905
+ const authClient = await auth.getClient();
906
+ const accessToken = await authClient.getAccessToken();
907
+
908
+ const catalogUrl = `https://gcr.io/v2/${projectId}/tags/list`;
909
+
910
+ const catalogResponse = await axios.get(catalogUrl, {
911
+ headers: {
912
+ Authorization: `Bearer ${accessToken.token}`,
913
+ Accept: "application/json",
914
+ },
915
+ timeout: 15000,
916
+ });
917
+
918
+ const repositories = catalogResponse.data.child || [];
919
+
920
+ if (repositories.length === 0) {
921
+ return [];
922
+ }
923
+
924
+ const imagePromises = repositories.slice(0, 20).map(async (repoName) => {
925
+ try {
926
+ const tagsUrl = `https://gcr.io/v2/${projectId}/${repoName}/tags/list`;
927
+
928
+ const tagsResponse = await axios.get(tagsUrl, {
929
+ headers: {
930
+ Authorization: `Bearer ${accessToken.token}`,
931
+ Accept: "application/json",
932
+ },
933
+ timeout: 10000,
934
+ });
935
+
936
+ const tags = tagsResponse.data.tags || [];
937
+
938
+ return tags.slice(0, 5).map((tag) => ({
939
+ id: `gcr-${repoName}-${tag}`,
940
+ name: `gcr.io/${projectId}/${repoName}`,
941
+ tag: tag,
942
+ size: "Unknown",
943
+ created: "Unknown",
944
+ registry: registry.id,
945
+ isPrivate: true,
946
+ pullCount: 0,
947
+ starCount: 0,
948
+ architecture: "amd64",
949
+ os: "linux",
950
+ }));
951
+ } catch (tagError) {
952
+ console.error(
953
+ chalk.red(
954
+ `❌ Error fetching tags for repository ${repoName}:`,
955
+ tagError.message
956
+ )
957
+ );
958
+ return [];
959
+ }
960
+ });
961
+
962
+ const imageArrays = await Promise.all(imagePromises);
963
+ const allImages = imageArrays.flat().filter((image) => image);
964
+
965
+ return allImages;
966
+ } catch (error) {
967
+ console.error(chalk.red("❌ Error fetching GCR images:", error));
968
+
969
+ if (error.response?.status === 401) {
970
+ throw new Error("Invalid service account credentials");
971
+ } else if (error.response?.status === 403) {
972
+ throw new Error("Service account lacks required permissions");
973
+ } else {
974
+ throw new Error(`GCR API error: ${error.message}`);
975
+ }
976
+ }
977
+ }
978
+
979
+ //! TO BE ENHANCED IN THE FUTURE
980
+ // Helper function to fetch images from Azure ACR
981
+ async function fetchACRImages(registry) {
982
+ try {
983
+ const registryName = registry.url
984
+ .replace("https://", "")
985
+ .replace(".azurecr.io", "");
986
+
987
+ const catalogUrl = `${registry.url}/v2/_catalog`;
988
+
989
+ const catalogResponse = await axios.get(catalogUrl, {
990
+ headers: {
991
+ Authorization: `Basic ${Buffer.from(
992
+ `${registry.credentials.username}:${registry.credentials.password}`
993
+ ).toString("base64")}`,
994
+ },
995
+ timeout: 15000,
996
+ });
997
+
998
+ const repositories = catalogResponse.data.repositories || [];
999
+
1000
+ if (repositories.length === 0) {
1001
+ return [];
1002
+ }
1003
+
1004
+ const imagePromises = repositories.slice(0, 20).map(async (repoName) => {
1005
+ try {
1006
+ const tagsUrl = `${registry.url}/v2/${repoName}/tags/list`;
1007
+
1008
+ const tagsResponse = await axios.get(tagsUrl, {
1009
+ headers: {
1010
+ Authorization: `Basic ${Buffer.from(
1011
+ `${registry.credentials.username}:${registry.credentials.password}`
1012
+ ).toString("base64")}`,
1013
+ },
1014
+ timeout: 10000,
1015
+ });
1016
+
1017
+ const tags = tagsResponse.data.tags || [];
1018
+
1019
+ return tags.slice(0, 5).map((tag) => ({
1020
+ id: `acr-${repoName}-${tag}`,
1021
+ name: `${registryName}.azurecr.io/${repoName}`,
1022
+ tag: tag,
1023
+ size: "Unknown",
1024
+ created: "Unknown",
1025
+ registry: registry.id,
1026
+ isPrivate: true,
1027
+ pullCount: 0,
1028
+ starCount: 0,
1029
+ architecture: "amd64",
1030
+ os: "linux",
1031
+ }));
1032
+ } catch (tagError) {
1033
+ console.error(
1034
+ chalk.red(
1035
+ `❌ Error fetching tags for repository ${repoName}:`,
1036
+ tagError.message
1037
+ )
1038
+ );
1039
+ return [];
1040
+ }
1041
+ });
1042
+
1043
+ const imageArrays = await Promise.all(imagePromises);
1044
+ const allImages = imageArrays.flat().filter((image) => image);
1045
+
1046
+ return allImages;
1047
+ } catch (error) {
1048
+ console.error(chalk.red("❌ Error fetching ACR images:", error));
1049
+
1050
+ if (error.response?.status === 401) {
1051
+ throw new Error("Invalid username or password/token");
1052
+ } else if (error.response?.status === 404) {
1053
+ throw new Error("Registry not found");
1054
+ } else {
1055
+ throw new Error(`ACR API error: ${error.message}`);
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ //! TO BE ENHANCED IN THE FUTURE
1061
+ // Helper function to fetch images from custom registry
1062
+ async function fetchCustomRegistryImages(registry) {
1063
+ try {
1064
+ const apiUrl = `${registry.url}/v2/_catalog`;
1065
+
1066
+ const response = await fetch(apiUrl, {
1067
+ headers: {
1068
+ Authorization: `Basic ${Buffer.from(
1069
+ `${registry.credentials.username}:${registry.credentials.password}`
1070
+ ).toString("base64")}`,
1071
+ },
1072
+ });
1073
+
1074
+ if (!response.ok) {
1075
+ throw new Error(
1076
+ `Registry API error: ${response.status} ${response.statusText}`
1077
+ );
1078
+ }
1079
+
1080
+ const data = await response.json();
1081
+
1082
+ const repositories = data.repositories || [];
1083
+
1084
+ return repositories.map((repo, index) => ({
1085
+ id: `custom-${index}-${repo}`,
1086
+ name: repo,
1087
+ tag: "latest",
1088
+ size: "Unknown",
1089
+ created: "Unknown",
1090
+ registry: registry.id,
1091
+ }));
1092
+ } catch (error) {
1093
+ console.error("Error fetching custom registry images:", error);
1094
+ throw error;
1095
+ }
1096
+ }
1097
+
1098
+ export default { handleRegistryAction };
1099
+
1100
+ export { handleRegistryAction };