carto-cli 0.1.0-rc.1

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,652 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildReauthCommand = buildReauthCommand;
4
+ exports.authLogin = authLogin;
5
+ exports.authLogout = authLogout;
6
+ exports.authUse = authUse;
7
+ exports.authStatus = authStatus;
8
+ exports.authWhoami = authWhoami;
9
+ exports.authLoginM2M = authLoginM2M;
10
+ const api_1 = require("../api");
11
+ const config_1 = require("../config");
12
+ const colors_1 = require("../colors");
13
+ const auth_server_1 = require("../auth-server");
14
+ const auth_oauth_1 = require("../auth-oauth");
15
+ const browser_1 = require("../browser");
16
+ /**
17
+ * Build the correct re-authentication command for the current profile and environment
18
+ * @param profileName Current profile name
19
+ * @param credentials Current profile credentials (null if not available)
20
+ * @returns Properly formatted carto auth login command
21
+ */
22
+ function buildReauthCommand(profileName, credentials) {
23
+ let cmd = 'carto auth login';
24
+ // Add profile name if not default profile
25
+ // Quote profile names with spaces or special characters
26
+ if (profileName && profileName !== 'default') {
27
+ const needsQuotes = profileName.includes(' ') || profileName.includes('@') || profileName.includes('/');
28
+ cmd += needsQuotes ? ` "${profileName}"` : ` ${profileName}`;
29
+ }
30
+ // Add --env flag if not production environment
31
+ if (credentials?.auth_environment && credentials.auth_environment !== 'production') {
32
+ cmd += ` --env ${credentials.auth_environment}`;
33
+ }
34
+ // Add --organization-name flag if organization was used during login
35
+ if (credentials?.organization_name_used) {
36
+ const orgName = credentials.organization_name_used;
37
+ const needsQuotes = orgName.includes(' ') || orgName.includes('@') || orgName.includes('/');
38
+ cmd += needsQuotes ? ` --organization-name "${orgName}"` : ` --organization-name ${orgName}`;
39
+ }
40
+ return cmd;
41
+ }
42
+ async function authLogin(profileName, jsonOutput, env, organizationName, organizationId) {
43
+ try {
44
+ // Fixed port for OAuth callback (authorized in Auth0)
45
+ const port = 3003;
46
+ // Get OAuth configuration
47
+ const config = (0, auth_oauth_1.getAuthConfig)(env);
48
+ // Get accounts URL for organization lookup
49
+ const accountsUrl = (0, auth_oauth_1.getAccountsUrl)(env || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT);
50
+ // Resolve Auth0 Organization ID if organization name provided
51
+ let auth0OrganizationId = null;
52
+ if (organizationName) {
53
+ if (!jsonOutput) {
54
+ console.log((0, colors_1.info)(`šŸ” Looking up organization: ${organizationName}...`));
55
+ }
56
+ try {
57
+ auth0OrganizationId = await (0, auth_oauth_1.getAuth0OrganizationIdByName)(accountsUrl, organizationName);
58
+ }
59
+ catch (err) {
60
+ if (jsonOutput) {
61
+ console.log(JSON.stringify({ success: false, error: `Failed to lookup organization: ${err.message}` }));
62
+ }
63
+ else {
64
+ console.log((0, colors_1.error)(`āœ— Failed to lookup organization: ${err.message}`));
65
+ console.log((0, colors_1.info)(' Tip: Verify the organization name is correct'));
66
+ }
67
+ process.exit(1);
68
+ }
69
+ if (!auth0OrganizationId) {
70
+ if (!jsonOutput) {
71
+ console.log((0, colors_1.warning)('⚠ SSO not configured for this organization'));
72
+ console.log((0, colors_1.info)(' Proceeding with standard login...'));
73
+ }
74
+ }
75
+ else {
76
+ if (!jsonOutput) {
77
+ console.log((0, colors_1.success)(`āœ“ SSO enabled - redirecting to organization login`));
78
+ }
79
+ }
80
+ }
81
+ else if (organizationId) {
82
+ if (jsonOutput) {
83
+ console.log(JSON.stringify({ success: false, error: 'Organization ID lookup not yet supported. Use --organization-name instead.' }));
84
+ }
85
+ else {
86
+ console.log((0, colors_1.error)(`āœ— Organization ID lookup not yet supported`));
87
+ console.log((0, colors_1.info)(' Tip: Use --organization-name with the organization name'));
88
+ }
89
+ process.exit(1);
90
+ }
91
+ // Generate PKCE challenge and state
92
+ const { verifier, challenge } = (0, auth_oauth_1.generatePKCEChallenge)();
93
+ const state = (0, auth_oauth_1.generateState)();
94
+ // Build redirect URI
95
+ const redirectUri = `http://localhost:${port}/callback`;
96
+ // Build authorization URL
97
+ const authUrl = (0, auth_oauth_1.buildAuthorizationUrl)(config, redirectUri, challenge, state, auth0OrganizationId || undefined);
98
+ if (!jsonOutput) {
99
+ console.log('');
100
+ console.log((0, colors_1.info)(auth0OrganizationId ? 'šŸ” SSO Authentication' : 'šŸ” CARTO CLI Authentication'));
101
+ // Only show auth environment for non-production
102
+ const envName = env || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT;
103
+ if (envName !== 'production') {
104
+ console.log((0, colors_1.info)(` Auth Environment: ${config.domain}`));
105
+ }
106
+ console.log('');
107
+ // Try to open browser automatically
108
+ try {
109
+ await (0, browser_1.openBrowser)(authUrl);
110
+ console.log((0, colors_1.success)('āœ“ Opening browser for authentication...'));
111
+ console.log('');
112
+ }
113
+ catch (browserError) {
114
+ // Fallback: show URL for manual opening
115
+ console.log((0, colors_1.warning)('⚠ Could not open browser automatically'));
116
+ console.log((0, colors_1.bold)('Please open this URL in your browser:'));
117
+ console.log('');
118
+ console.log((0, colors_1.dim)(' ') + authUrl);
119
+ console.log('');
120
+ }
121
+ console.log((0, colors_1.info)(`ā³ Waiting for authentication (listening on http://localhost:${port}/callback)...`));
122
+ console.log('');
123
+ }
124
+ // Start local callback server
125
+ const serverPromise = (0, auth_server_1.startOAuthCallbackServer)(port, state);
126
+ // Wait for OAuth callback
127
+ const { code } = await serverPromise;
128
+ if (!jsonOutput) {
129
+ console.log((0, colors_1.info)('šŸ”„ Exchanging authorization code for access token...'));
130
+ }
131
+ // Exchange authorization code for access token
132
+ const tokenResponse = await (0, auth_oauth_1.exchangeCodeForToken)(config, code, verifier, redirectUri);
133
+ // Extract token
134
+ const accessToken = tokenResponse.access_token;
135
+ // Fetch full user info from CARTO accounts API (environment-specific)
136
+ // Use createMinimal because credentials don't exist yet during login
137
+ const tempClient = api_1.ApiClient.createMinimal(accessToken);
138
+ const userInfo = await tempClient.getFullUrl(`${accountsUrl}/users/me`, true);
139
+ // Build full credentials with org and user info
140
+ const fullCredentials = {
141
+ token: accessToken,
142
+ tenant_id: userInfo.user_metadata?.tenant_id || '',
143
+ tenant_domain: userInfo.user_metadata?.tenant_domain || '',
144
+ organization_id: userInfo.user_metadata?.account_id || '',
145
+ organization_name: userInfo.user_metadata?.account_name || '',
146
+ user_email: userInfo.email || '',
147
+ auth_environment: env || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT,
148
+ organization_name_used: organizationName || undefined,
149
+ };
150
+ // Determine profile name
151
+ let finalProfileName = profileName;
152
+ if (!finalProfileName) {
153
+ // Generate suggested name
154
+ finalProfileName = (0, config_1.suggestProfileName)(fullCredentials);
155
+ }
156
+ // Save credentials to file
157
+ (0, config_1.saveCredentials)(finalProfileName, fullCredentials);
158
+ if (jsonOutput) {
159
+ console.log(JSON.stringify({
160
+ success: true,
161
+ message: 'Authentication successful',
162
+ profile: finalProfileName,
163
+ tenant_id: fullCredentials.tenant_id,
164
+ tenant_domain: fullCredentials.tenant_domain,
165
+ organization_id: fullCredentials.organization_id,
166
+ organization_name: fullCredentials.organization_name,
167
+ user_email: fullCredentials.user_email,
168
+ }));
169
+ }
170
+ else {
171
+ console.log((0, colors_1.success)('\nāœ“ Authentication successful!'));
172
+ if (auth0OrganizationId) {
173
+ console.log((0, colors_1.success)(' Authenticated via SSO'));
174
+ }
175
+ console.log((0, colors_1.info)(` Tenant: ${fullCredentials.tenant_domain} (${fullCredentials.tenant_id})`));
176
+ console.log((0, colors_1.info)(` Organization: ${fullCredentials.organization_name} (${fullCredentials.organization_id})`));
177
+ console.log((0, colors_1.info)(` User: ${fullCredentials.user_email}`));
178
+ console.log('');
179
+ console.log((0, colors_1.success)(`āœ“ Credentials saved as profile: ${(0, colors_1.bold)(finalProfileName)}`));
180
+ console.log((0, colors_1.success)(`āœ“ Set as current profile`));
181
+ }
182
+ }
183
+ catch (err) {
184
+ if (jsonOutput) {
185
+ console.log(JSON.stringify({ success: false, error: err.message }));
186
+ }
187
+ else {
188
+ console.log((0, colors_1.error)('\nāœ— Authentication failed: ' + err.message));
189
+ }
190
+ process.exit(1);
191
+ }
192
+ }
193
+ async function authLogout(profileName, jsonOutput) {
194
+ try {
195
+ const targetProfile = profileName || (0, config_1.getCurrentProfile)();
196
+ // Delete credentials
197
+ (0, config_1.deleteCredentials)(targetProfile);
198
+ // Also delete legacy config if removing default profile
199
+ if (targetProfile === 'default') {
200
+ try {
201
+ (0, config_1.deleteConfig)();
202
+ }
203
+ catch {
204
+ // Ignore if legacy config doesn't exist
205
+ }
206
+ }
207
+ if (jsonOutput) {
208
+ console.log(JSON.stringify({ success: true, message: 'Credentials removed successfully', profile: targetProfile }));
209
+ }
210
+ else {
211
+ console.log((0, colors_1.success)(`āœ“ Credentials removed successfully from profile: ${(0, colors_1.bold)(targetProfile)}`));
212
+ }
213
+ }
214
+ catch (err) {
215
+ if (jsonOutput) {
216
+ console.log(JSON.stringify({ success: false, error: err.message }));
217
+ }
218
+ else {
219
+ console.log((0, colors_1.error)('āœ— Failed to remove credentials: ' + err.message));
220
+ }
221
+ process.exit(1);
222
+ }
223
+ }
224
+ async function authUse(profileName, jsonOutput) {
225
+ try {
226
+ const allCreds = (0, config_1.loadAllCredentials)();
227
+ if (!allCreds.profiles[profileName]) {
228
+ const available = Object.keys(allCreds.profiles);
229
+ if (jsonOutput) {
230
+ console.log(JSON.stringify({
231
+ success: false,
232
+ error: `Profile '${profileName}' not found`,
233
+ availableProfiles: available
234
+ }));
235
+ }
236
+ else {
237
+ console.log((0, colors_1.error)(`āœ— Profile '${profileName}' not found`));
238
+ if (available.length > 0) {
239
+ console.log((0, colors_1.info)('\nAvailable profiles:'));
240
+ available.forEach(p => console.log((0, colors_1.info)(` - ${p}`)));
241
+ }
242
+ else {
243
+ console.log((0, colors_1.info)('\nNo profiles found. Run "carto auth login" to authenticate.'));
244
+ }
245
+ }
246
+ process.exit(1);
247
+ }
248
+ (0, config_1.setCurrentProfile)(profileName);
249
+ const credentials = allCreds.profiles[profileName];
250
+ if (jsonOutput) {
251
+ console.log(JSON.stringify({
252
+ success: true,
253
+ message: 'Switched profile successfully',
254
+ profile: profileName,
255
+ tenant_id: credentials.tenant_id,
256
+ tenant_domain: credentials.tenant_domain,
257
+ organization_id: credentials.organization_id,
258
+ organization_name: credentials.organization_name,
259
+ user_email: credentials.user_email,
260
+ }));
261
+ }
262
+ else {
263
+ console.log((0, colors_1.success)(`āœ“ Switched to profile: ${(0, colors_1.bold)(profileName)}`));
264
+ console.log((0, colors_1.info)(` Tenant: ${credentials.tenant_domain} (${credentials.tenant_id})`));
265
+ console.log((0, colors_1.info)(` Organization: ${credentials.organization_name} (${credentials.organization_id})`));
266
+ console.log((0, colors_1.info)(` User: ${credentials.user_email}`));
267
+ }
268
+ }
269
+ catch (err) {
270
+ if (jsonOutput) {
271
+ console.log(JSON.stringify({ success: false, error: err.message }));
272
+ }
273
+ else {
274
+ console.log((0, colors_1.error)('āœ— Failed to switch profile: ' + err.message));
275
+ }
276
+ process.exit(1);
277
+ }
278
+ }
279
+ async function authStatus(profileName, jsonOutput) {
280
+ const currentProfile = profileName || (0, config_1.getCurrentProfile)();
281
+ const token = (0, config_1.getApiToken)(currentProfile);
282
+ const credentials = (0, config_1.loadCredentials)(currentProfile);
283
+ const config = (0, config_1.loadConfig)();
284
+ const allCredentials = (0, config_1.loadAllCredentials)();
285
+ const availableProfiles = Object.keys(allCredentials.profiles);
286
+ let tokenSource = 'none';
287
+ if (process.env.CARTO_API_TOKEN) {
288
+ tokenSource = 'environment';
289
+ }
290
+ else if (credentials) {
291
+ tokenSource = 'credentials';
292
+ }
293
+ else if (currentProfile === 'default' && config.apiToken) {
294
+ tokenSource = 'legacy_config';
295
+ }
296
+ // Get token expiration information
297
+ let tokenExpired = false;
298
+ let tokenExpiring = false;
299
+ let tokenExpirationTimestamp = null;
300
+ let tokenTimeRemaining = null;
301
+ let tokenExpirationFormatted = null;
302
+ if (token) {
303
+ tokenExpired = (0, auth_oauth_1.isTokenExpired)(token);
304
+ tokenExpiring = (0, auth_oauth_1.shouldWarnExpiration)(token);
305
+ tokenExpirationTimestamp = (0, auth_oauth_1.getTokenExpiration)(token);
306
+ tokenTimeRemaining = (0, auth_oauth_1.getTokenTimeRemaining)(token);
307
+ if (tokenExpirationTimestamp) {
308
+ const date = new Date(tokenExpirationTimestamp * 1000);
309
+ tokenExpirationFormatted = date.toLocaleString();
310
+ }
311
+ }
312
+ if (jsonOutput) {
313
+ console.log(JSON.stringify({
314
+ authenticated: !!token,
315
+ tokenSource,
316
+ currentProfile: allCredentials.current_profile,
317
+ requestedProfile: currentProfile,
318
+ credentialsFile: '~/.carto_credentials.json',
319
+ tenant_id: credentials?.tenant_id,
320
+ tenant_domain: credentials?.tenant_domain,
321
+ organization_id: credentials?.organization_id,
322
+ organization_name: credentials?.organization_name,
323
+ user_email: credentials?.user_email,
324
+ availableProfiles: availableProfiles,
325
+ tokenExpired,
326
+ tokenExpiring,
327
+ tokenExpirationTimestamp,
328
+ tokenExpirationFormatted,
329
+ tokenTimeRemaining,
330
+ }));
331
+ }
332
+ else {
333
+ if (token) {
334
+ // Show authentication status with expiration awareness
335
+ if (tokenExpired) {
336
+ console.log((0, colors_1.error)('āœ— Token expired'));
337
+ }
338
+ else if (tokenExpiring) {
339
+ console.log((0, colors_1.warning)('⚠ Token expiring soon'));
340
+ }
341
+ else {
342
+ console.log((0, colors_1.success)('āœ“ Authenticated'));
343
+ }
344
+ console.log('');
345
+ console.log((0, colors_1.bold)(`Current profile: ${currentProfile}`) + (allCredentials.current_profile === currentProfile ? ' (default)' : ''));
346
+ console.log((0, colors_1.info)(` Token source: ${tokenSource}`));
347
+ console.log((0, colors_1.info)(` Token: ${token.substring(0, 20)}...`));
348
+ // Show expiration information if available
349
+ if (tokenExpirationTimestamp) {
350
+ console.log((0, colors_1.info)(` Expiration: ${tokenExpirationFormatted}`));
351
+ if (tokenTimeRemaining !== null) {
352
+ const timeStr = (0, auth_oauth_1.formatTimeRemaining)(tokenTimeRemaining);
353
+ const timeDisplay = tokenExpired ? (0, colors_1.error)(`(${timeStr})`) :
354
+ tokenExpiring ? (0, colors_1.warning)(`(in ${timeStr})`) :
355
+ (0, colors_1.info)(`(in ${timeStr})`);
356
+ console.log((0, colors_1.info)(` Time remaining: `) + timeDisplay);
357
+ }
358
+ // Status indicator
359
+ const statusEmoji = tokenExpired ? 'šŸ”“' : tokenExpiring ? '🟔' : '🟢';
360
+ const statusText = tokenExpired ? 'Expired' : tokenExpiring ? 'Expiring soon' : 'Valid';
361
+ const statusColor = tokenExpired ? (0, colors_1.error)(statusText) : tokenExpiring ? (0, colors_1.warning)(statusText) : (0, colors_1.success)(statusText);
362
+ console.log((0, colors_1.info)(` Status: ${statusEmoji} `) + statusColor);
363
+ }
364
+ if (credentials) {
365
+ console.log('');
366
+ // Only show auth environment for non-production
367
+ if (credentials.auth_environment && credentials.auth_environment !== 'production') {
368
+ console.log((0, colors_1.info)(` Auth Environment: ${credentials.auth_environment}`));
369
+ }
370
+ console.log((0, colors_1.info)(` Tenant: ${credentials.tenant_domain} (${credentials.tenant_id})`));
371
+ if (credentials.organization_name) {
372
+ console.log((0, colors_1.info)(` Organization: ${credentials.organization_name} (${credentials.organization_id})`));
373
+ }
374
+ if (credentials.user_email) {
375
+ console.log((0, colors_1.info)(` User: ${credentials.user_email}`));
376
+ }
377
+ }
378
+ // Show available profiles
379
+ if (availableProfiles.length > 0) {
380
+ console.log((0, colors_1.info)(`\nAvailable profiles (${availableProfiles.length}):`));
381
+ availableProfiles.forEach((profile) => {
382
+ const profileCreds = allCredentials.profiles[profile];
383
+ const isCurrent = profile === currentProfile;
384
+ const marker = isCurrent ? '→' : ' ';
385
+ // Only show auth environment label for non-production
386
+ const envLabel = (profileCreds.auth_environment && profileCreds.auth_environment !== 'production')
387
+ ? ` [${profileCreds.auth_environment}]`
388
+ : '';
389
+ console.log((0, colors_1.info)(` ${marker} ${(0, colors_1.bold)(profile)}${envLabel}`));
390
+ console.log((0, colors_1.info)(` Tenant: ${profileCreds.tenant_domain} (${profileCreds.tenant_id})`));
391
+ if (profileCreds.organization_name) {
392
+ console.log((0, colors_1.info)(` Organization: ${profileCreds.organization_name} (${profileCreds.organization_id})`));
393
+ }
394
+ if (profileCreds.user_email) {
395
+ console.log((0, colors_1.info)(` User: ${profileCreds.user_email}`));
396
+ }
397
+ });
398
+ console.log('');
399
+ console.log((0, colors_1.dim)(`Use "carto auth use <profile>" to switch profiles`));
400
+ }
401
+ // Show warnings/errors for token expiration
402
+ const reauthCmd = buildReauthCommand(currentProfile, credentials);
403
+ if (tokenExpired) {
404
+ console.log('');
405
+ if (tokenSource === 'environment') {
406
+ console.log((0, colors_1.error)('šŸ”“ Token has EXPIRED (from CARTO_API_TOKEN environment variable)'));
407
+ console.log((0, colors_1.error)(' Please update your CARTO_API_TOKEN with a fresh token'));
408
+ }
409
+ else {
410
+ console.log((0, colors_1.error)('šŸ”“ Token has EXPIRED'));
411
+ console.log((0, colors_1.error)(' Please re-authenticate to continue using the CLI:'));
412
+ console.log((0, colors_1.bold)(' ' + reauthCmd));
413
+ }
414
+ }
415
+ else if (tokenExpiring && tokenTimeRemaining !== null) {
416
+ console.log('');
417
+ console.log((0, colors_1.warning)('āš ļø Warning: Token is expiring soon'));
418
+ console.log((0, colors_1.warning)(` Less than 10% of token lifetime remaining`));
419
+ if (tokenSource === 'environment') {
420
+ console.log((0, colors_1.warning)(' Consider updating your CARTO_API_TOKEN with a fresh token'));
421
+ }
422
+ else {
423
+ console.log((0, colors_1.warning)(' Consider re-authenticating to avoid interruptions:'));
424
+ console.log((0, colors_1.bold)(' ' + reauthCmd));
425
+ }
426
+ }
427
+ }
428
+ else {
429
+ console.log((0, colors_1.error)('āœ— Not authenticated'));
430
+ // Build suggested login command
431
+ // For not-authenticated case, we can't load credentials, so just use profile name
432
+ let loginCmd = 'carto auth login';
433
+ if (currentProfile && currentProfile !== 'default') {
434
+ const needsQuotes = currentProfile.includes(' ') || currentProfile.includes('@') || currentProfile.includes('/');
435
+ loginCmd += needsQuotes ? ` "${currentProfile}"` : ` ${currentProfile}`;
436
+ }
437
+ console.log((0, colors_1.info)(`Use "${loginCmd}" to authenticate`));
438
+ // Show available profiles even when not authenticated with current profile
439
+ if (availableProfiles.length > 0) {
440
+ console.log((0, colors_1.info)(`\nAvailable profiles (${availableProfiles.length}):`));
441
+ availableProfiles.forEach((profile) => {
442
+ const profileCreds = allCredentials.profiles[profile];
443
+ const isCurrent = profile === currentProfile;
444
+ const marker = isCurrent ? '→' : ' ';
445
+ console.log((0, colors_1.info)(` ${marker} ${(0, colors_1.bold)(profile)}: ${profileCreds.tenant_domain}`));
446
+ });
447
+ }
448
+ }
449
+ }
450
+ }
451
+ async function authWhoami(token, baseUrl, jsonOutput, debug = false, profile) {
452
+ try {
453
+ const client = await api_1.ApiClient.create(token, baseUrl, debug, profile);
454
+ // Get accounts URL from profile credentials (environment-specific)
455
+ const profileName = profile || (0, config_1.getCurrentProfile)();
456
+ const credentials = (0, config_1.loadCredentials)(profileName);
457
+ const accountsUrl = (0, auth_oauth_1.getAccountsUrl)(credentials?.auth_environment || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT);
458
+ // Call the user info endpoint
459
+ const userInfo = await client.getFullUrl(`${accountsUrl}/users/me`, true);
460
+ if (jsonOutput) {
461
+ console.log(JSON.stringify(userInfo));
462
+ }
463
+ else {
464
+ console.log((0, colors_1.bold)('\n=== Current User ===\n'));
465
+ console.log((0, colors_1.bold)('User ID: ') + userInfo.user_id);
466
+ console.log((0, colors_1.bold)('Name: ') + (userInfo.name || 'N/A'));
467
+ console.log((0, colors_1.bold)('Email: ') + userInfo.email);
468
+ console.log((0, colors_1.bold)('Nickname: ') + (userInfo.nickname || 'N/A'));
469
+ if (userInfo.user_metadata) {
470
+ console.log((0, colors_1.bold)('\nAccount Info:'));
471
+ console.log(' Account ID: ' + userInfo.user_metadata.account_id);
472
+ console.log(' Account Name: ' + userInfo.user_metadata.account_name);
473
+ console.log(' Tenant: ' + userInfo.user_metadata.tenant_domain + ' (' + userInfo.user_metadata.tenant_id + ')');
474
+ }
475
+ if (userInfo.app_metadata?.roles && userInfo.app_metadata.roles.length > 0) {
476
+ console.log((0, colors_1.bold)('\nRoles: ') + userInfo.app_metadata.roles.join(', '));
477
+ }
478
+ console.log((0, colors_1.bold)('\nEmail Verified: ') + (userInfo.email_verified ? 'Yes' : 'No'));
479
+ console.log((0, colors_1.bold)('Created: ') + new Date(userInfo.created_at).toLocaleString());
480
+ }
481
+ }
482
+ catch (err) {
483
+ if (jsonOutput) {
484
+ console.log(JSON.stringify({ success: false, error: err.message }));
485
+ }
486
+ else {
487
+ if (err.message.includes('401') || err.message.includes('Token not defined')) {
488
+ console.log((0, colors_1.error)('āœ— Authentication required'));
489
+ console.log('Please run: carto auth login');
490
+ }
491
+ else {
492
+ console.log((0, colors_1.error)('āœ— Failed to get user info: ' + err.message));
493
+ }
494
+ }
495
+ process.exit(1);
496
+ }
497
+ }
498
+ /**
499
+ * M2M (Machine-to-Machine) OAuth login for CI/CD environments
500
+ * Uses OAuth client_credentials grant - no browser interaction required
501
+ */
502
+ async function authLoginM2M(profileName, jsonOutput, env, clientId, clientSecret, apiBaseUrl, force) {
503
+ try {
504
+ // Determine profile name early - for M2M, default to 'm2m' if not specified
505
+ const targetProfileName = profileName || 'm2m';
506
+ // Check for existing valid token (unless --force is used)
507
+ if (!force) {
508
+ const existingCredentials = (0, config_1.loadCredentials)(targetProfileName);
509
+ if (existingCredentials?.token) {
510
+ const tokenExpired = (0, auth_oauth_1.isTokenExpired)(existingCredentials.token);
511
+ const timeRemaining = (0, auth_oauth_1.getTokenTimeRemaining)(existingCredentials.token);
512
+ if (!tokenExpired && timeRemaining !== null && timeRemaining > 0) {
513
+ // Token is still valid - reuse it
514
+ if (jsonOutput) {
515
+ console.log(JSON.stringify({
516
+ success: true,
517
+ message: 'Using existing valid M2M token',
518
+ profile: targetProfileName,
519
+ organization_id: existingCredentials.organization_id || null,
520
+ api_base_url: existingCredentials.base_url,
521
+ type: 'm2m-service-account',
522
+ time_remaining_seconds: timeRemaining,
523
+ reused: true
524
+ }));
525
+ }
526
+ else {
527
+ console.log('');
528
+ console.log((0, colors_1.success)(`āœ“ Valid M2M token found for profile: ${(0, colors_1.bold)(targetProfileName)}`));
529
+ console.log((0, colors_1.info)(` Time remaining: ${(0, auth_oauth_1.formatTimeRemaining)(timeRemaining)}`));
530
+ if (existingCredentials.organization_id) {
531
+ console.log((0, colors_1.info)(` Organization: ${existingCredentials.organization_id}`));
532
+ }
533
+ console.log((0, colors_1.info)(` API Base URL: ${existingCredentials.base_url}`));
534
+ console.log('');
535
+ console.log((0, colors_1.dim)(' Use --force to request a new token'));
536
+ }
537
+ // Set as current profile
538
+ (0, config_1.setCurrentProfile)(targetProfileName);
539
+ return;
540
+ }
541
+ }
542
+ }
543
+ // Get credentials from: flags > env vars
544
+ const finalClientId = clientId || process.env.CARTO_M2M_CLIENT_ID;
545
+ const finalClientSecret = clientSecret || process.env.CARTO_M2M_CLIENT_SECRET;
546
+ if (!finalClientId || !finalClientSecret) {
547
+ const missing = [];
548
+ if (!finalClientId)
549
+ missing.push('client_id');
550
+ if (!finalClientSecret)
551
+ missing.push('client_secret');
552
+ if (jsonOutput) {
553
+ console.log(JSON.stringify({
554
+ success: false,
555
+ error: `Missing M2M credentials: ${missing.join(', ')}`,
556
+ hint: 'Provide via --client-id/--client-secret flags or CARTO_M2M_CLIENT_ID/CARTO_M2M_CLIENT_SECRET environment variables'
557
+ }));
558
+ }
559
+ else {
560
+ console.log((0, colors_1.error)(`āœ— Missing M2M credentials: ${missing.join(', ')}`));
561
+ console.log((0, colors_1.info)(' Provide via flags:'));
562
+ console.log((0, colors_1.info)(' carto auth login --m2m --client-id <id> --client-secret <secret>'));
563
+ console.log((0, colors_1.info)(' Or via environment variables:'));
564
+ console.log((0, colors_1.info)(' export CARTO_M2M_CLIENT_ID="your-client-id"'));
565
+ console.log((0, colors_1.info)(' export CARTO_M2M_CLIENT_SECRET="your-client-secret"'));
566
+ console.log((0, colors_1.info)(' carto auth login --m2m'));
567
+ }
568
+ process.exit(1);
569
+ }
570
+ // Get M2M auth config for the environment
571
+ const m2mConfig = (0, auth_oauth_1.getM2MAuthConfig)(env);
572
+ if (!jsonOutput) {
573
+ console.log('');
574
+ console.log((0, colors_1.info)('šŸ” M2M Authentication (CI/CD mode)'));
575
+ const envName = env || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT;
576
+ if (envName !== 'production') {
577
+ console.log((0, colors_1.info)(` Auth Environment: ${m2mConfig.domain}`));
578
+ }
579
+ console.log('');
580
+ console.log((0, colors_1.info)('šŸ”„ Exchanging M2M credentials for access token...'));
581
+ }
582
+ // Exchange M2M credentials for token
583
+ const tokenResponse = await (0, auth_oauth_1.exchangeM2MCredentialsForToken)(m2mConfig, finalClientId, finalClientSecret);
584
+ const accessToken = tokenResponse.access_token;
585
+ if (!jsonOutput) {
586
+ console.log((0, colors_1.success)('āœ“ Token obtained successfully'));
587
+ }
588
+ // M2M tokens don't have a user - extract info from JWT claims
589
+ let organizationId = '';
590
+ try {
591
+ const tokenPayload = (0, auth_oauth_1.decodeJWT)(accessToken);
592
+ // CARTO M2M tokens include account info in http://app.carto.com/account_id claim
593
+ organizationId = tokenPayload['http://app.carto.com/account_id'] || '';
594
+ }
595
+ catch (decodeErr) {
596
+ // If JWT decode fails, continue with empty values
597
+ if (!jsonOutput) {
598
+ console.log((0, colors_1.warning)('⚠ Could not extract details from token'));
599
+ }
600
+ }
601
+ // For M2M, we need the API base URL - default to production gcp-us-east1
602
+ const finalBaseUrl = apiBaseUrl || process.env.CARTO_API_BASE_URL || 'https://gcp-us-east1.api.carto.com';
603
+ // Build credentials object - M2M stores base_url directly since there's no tenant lookup
604
+ const fullCredentials = {
605
+ token: accessToken,
606
+ tenant_id: '',
607
+ tenant_domain: '',
608
+ organization_id: organizationId,
609
+ organization_name: '',
610
+ user_email: 'm2m-service-account',
611
+ auth_environment: env || auth_oauth_1.DEFAULT_AUTH_ENVIRONMENT,
612
+ base_url: finalBaseUrl,
613
+ };
614
+ // Save credentials (targetProfileName was determined at the start of the function)
615
+ (0, config_1.saveCredentials)(targetProfileName, fullCredentials);
616
+ if (jsonOutput) {
617
+ console.log(JSON.stringify({
618
+ success: true,
619
+ message: 'M2M authentication successful',
620
+ profile: targetProfileName,
621
+ organization_id: fullCredentials.organization_id || null,
622
+ api_base_url: fullCredentials.base_url,
623
+ type: 'm2m-service-account',
624
+ expires_in: tokenResponse.expires_in,
625
+ }));
626
+ }
627
+ else {
628
+ console.log((0, colors_1.success)('\nāœ“ M2M authentication successful!'));
629
+ if (fullCredentials.organization_id) {
630
+ console.log((0, colors_1.info)(` Organization: ${fullCredentials.organization_id}`));
631
+ }
632
+ console.log((0, colors_1.info)(` API Base URL: ${fullCredentials.base_url}`));
633
+ console.log((0, colors_1.info)(` Type: M2M Service Account`));
634
+ console.log((0, colors_1.info)(` Token expires in: ${Math.floor(tokenResponse.expires_in / 3600)} hours`));
635
+ console.log('');
636
+ console.log((0, colors_1.success)(`āœ“ Credentials saved as profile: ${(0, colors_1.bold)(targetProfileName)}`));
637
+ console.log((0, colors_1.success)(`āœ“ Set as current profile`));
638
+ }
639
+ }
640
+ catch (err) {
641
+ if (jsonOutput) {
642
+ console.log(JSON.stringify({ success: false, error: err.message }));
643
+ }
644
+ else {
645
+ console.log((0, colors_1.error)('\nāœ— M2M authentication failed: ' + err.message));
646
+ if (err.message.includes('Invalid M2M credentials')) {
647
+ console.log((0, colors_1.info)(' Check your CARTO_M2M_CLIENT_ID and CARTO_M2M_CLIENT_SECRET values'));
648
+ }
649
+ }
650
+ process.exit(1);
651
+ }
652
+ }