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.
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +497 -0
- package/CHANGELOG.md +28 -0
- package/LICENSE +15 -0
- package/MAP_JSON.md +516 -0
- package/README.md +1595 -0
- package/WORKFLOW_JSON.md +623 -0
- package/dist/api.js +489 -0
- package/dist/auth-oauth.js +485 -0
- package/dist/auth-server.js +432 -0
- package/dist/browser.js +30 -0
- package/dist/colors.js +45 -0
- package/dist/commands/activity.js +427 -0
- package/dist/commands/admin.js +177 -0
- package/dist/commands/ai.js +489 -0
- package/dist/commands/auth.js +652 -0
- package/dist/commands/connections.js +412 -0
- package/dist/commands/credentials.js +606 -0
- package/dist/commands/imports.js +234 -0
- package/dist/commands/maps.js +1022 -0
- package/dist/commands/org.js +195 -0
- package/dist/commands/sql.js +326 -0
- package/dist/commands/users.js +459 -0
- package/dist/commands/workflows.js +1025 -0
- package/dist/config.js +320 -0
- package/dist/download.js +108 -0
- package/dist/help.js +285 -0
- package/dist/http.js +139 -0
- package/dist/index.js +1133 -0
- package/dist/logo.js +11 -0
- package/dist/prompt.js +67 -0
- package/dist/schedule-parser.js +287 -0
- package/jest.config.ts +43 -0
- package/package.json +53 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTH_CONFIGS = exports.M2M_AUTH_CONFIGS = exports.DEFAULT_AUTH_ENVIRONMENT = void 0;
|
|
4
|
+
exports.parseEnvironment = parseEnvironment;
|
|
5
|
+
exports.getAccountsUrl = getAccountsUrl;
|
|
6
|
+
exports.getAuthConfig = getAuthConfig;
|
|
7
|
+
exports.generateCodeVerifier = generateCodeVerifier;
|
|
8
|
+
exports.generateCodeChallenge = generateCodeChallenge;
|
|
9
|
+
exports.generatePKCEChallenge = generatePKCEChallenge;
|
|
10
|
+
exports.generateState = generateState;
|
|
11
|
+
exports.buildAuthorizationUrl = buildAuthorizationUrl;
|
|
12
|
+
exports.getAuth0OrganizationIdByName = getAuth0OrganizationIdByName;
|
|
13
|
+
exports.exchangeCodeForToken = exchangeCodeForToken;
|
|
14
|
+
exports.decodeJWT = decodeJWT;
|
|
15
|
+
exports.getTokenExpiration = getTokenExpiration;
|
|
16
|
+
exports.getTokenIssuedAt = getTokenIssuedAt;
|
|
17
|
+
exports.isTokenExpired = isTokenExpired;
|
|
18
|
+
exports.getTokenLifetime = getTokenLifetime;
|
|
19
|
+
exports.getTokenTimeRemaining = getTokenTimeRemaining;
|
|
20
|
+
exports.shouldWarnExpiration = shouldWarnExpiration;
|
|
21
|
+
exports.formatTimeRemaining = formatTimeRemaining;
|
|
22
|
+
exports.getM2MAuthConfig = getM2MAuthConfig;
|
|
23
|
+
exports.exchangeM2MCredentialsForToken = exchangeM2MCredentialsForToken;
|
|
24
|
+
const crypto_1 = require("crypto");
|
|
25
|
+
const https_1 = require("https");
|
|
26
|
+
// Default authentication environment when --env flag is not provided
|
|
27
|
+
exports.DEFAULT_AUTH_ENVIRONMENT = 'production';
|
|
28
|
+
exports.M2M_AUTH_CONFIGS = {
|
|
29
|
+
'production': {
|
|
30
|
+
domain: 'auth.carto.com',
|
|
31
|
+
audience: 'carto-cloud-native-api'
|
|
32
|
+
},
|
|
33
|
+
'staging': {
|
|
34
|
+
domain: 'auth.stag.carto.com',
|
|
35
|
+
audience: 'carto-cloud-native-api'
|
|
36
|
+
},
|
|
37
|
+
'local': {
|
|
38
|
+
domain: 'auth.local.carto.com',
|
|
39
|
+
audience: 'carto-cloud-native-api'
|
|
40
|
+
},
|
|
41
|
+
'dedicated': {
|
|
42
|
+
domain: 'auth.dev.carto.com',
|
|
43
|
+
audience: 'carto-cloud-native-api'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
// Environment configurations
|
|
47
|
+
// Note: dedicated-XX environments share the same auth domain but have different accounts URLs
|
|
48
|
+
exports.AUTH_CONFIGS = {
|
|
49
|
+
'local': {
|
|
50
|
+
domain: 'auth.local.carto.com',
|
|
51
|
+
clientId: '6EPxnXidfZZicawFOZ4Vw35bI5ONSmnP',
|
|
52
|
+
audience: 'carto-cloud-native-api'
|
|
53
|
+
},
|
|
54
|
+
'dedicated': {
|
|
55
|
+
domain: 'auth.dev.carto.com',
|
|
56
|
+
clientId: 'mBhNaKPjgpiCr92t1ljs5NVn2ODrev7p',
|
|
57
|
+
audience: 'carto-cloud-native-api'
|
|
58
|
+
},
|
|
59
|
+
'staging': {
|
|
60
|
+
domain: 'auth.stag.carto.com',
|
|
61
|
+
clientId: 'SezYkOlDCEiX46j6ljkKW2BbyNqKq82G',
|
|
62
|
+
audience: 'carto-cloud-native-api'
|
|
63
|
+
},
|
|
64
|
+
'production': {
|
|
65
|
+
domain: 'auth.carto.com',
|
|
66
|
+
clientId: '6eeVNUr9Ss2u3h972UPShWFRbKbkZHSR',
|
|
67
|
+
audience: 'carto-cloud-native-api'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Parse environment string to extract type and dedicated ID
|
|
72
|
+
* Examples: "dedicated-01" -> {type: "dedicated", dedicatedId: "01"}
|
|
73
|
+
* "production" -> {type: "production"}
|
|
74
|
+
*/
|
|
75
|
+
function parseEnvironment(env) {
|
|
76
|
+
// Match dedicated-NN pattern
|
|
77
|
+
const dedicatedMatch = env.match(/^dedicated-(\d+)$/);
|
|
78
|
+
if (dedicatedMatch) {
|
|
79
|
+
return {
|
|
80
|
+
type: 'dedicated',
|
|
81
|
+
dedicatedId: dedicatedMatch[1]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Check if it's a known base environment
|
|
85
|
+
if (env === 'local' || env === 'staging' || env === 'production') {
|
|
86
|
+
return { type: env };
|
|
87
|
+
}
|
|
88
|
+
// If user provides bare "dedicated", throw error
|
|
89
|
+
if (env === 'dedicated') {
|
|
90
|
+
throw new Error('Environment "dedicated" requires a specific ID. Use format: dedicated-01, dedicated-02, etc.');
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Unknown environment: ${env}. Valid formats: local, staging, production, dedicated-NN`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get accounts API URL for the specified environment
|
|
96
|
+
*
|
|
97
|
+
* @param env Environment string (e.g., "dedicated-26", "production")
|
|
98
|
+
* @returns Accounts API base URL
|
|
99
|
+
*/
|
|
100
|
+
function getAccountsUrl(env) {
|
|
101
|
+
const parsed = parseEnvironment(env);
|
|
102
|
+
switch (parsed.type) {
|
|
103
|
+
case 'dedicated':
|
|
104
|
+
// Pattern: https://accounts-{N}.dev.app.carto.com
|
|
105
|
+
return `https://accounts-${parsed.dedicatedId}.dev.app.carto.com`;
|
|
106
|
+
case 'local':
|
|
107
|
+
return 'http://localhost:8000';
|
|
108
|
+
case 'staging':
|
|
109
|
+
return 'https://accounts.stag.app.carto.com';
|
|
110
|
+
case 'production':
|
|
111
|
+
return 'https://accounts.app.carto.com';
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(`Unable to determine accounts URL for environment: ${env}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get OAuth config for authentication
|
|
118
|
+
*
|
|
119
|
+
* Environment selection priority:
|
|
120
|
+
* 1. Explicit environment override (e.g., dedicated-01, production)
|
|
121
|
+
* 2. CARTO_AUTH_ENV environment variable
|
|
122
|
+
* 3. Default: production
|
|
123
|
+
*
|
|
124
|
+
* @param envOverride Optional environment override
|
|
125
|
+
*/
|
|
126
|
+
function getAuthConfig(envOverride) {
|
|
127
|
+
const env = envOverride || process.env.CARTO_AUTH_ENV || exports.DEFAULT_AUTH_ENVIRONMENT;
|
|
128
|
+
// Parse environment to get the base type
|
|
129
|
+
const parsed = parseEnvironment(env);
|
|
130
|
+
// Return config for the base type
|
|
131
|
+
const config = exports.AUTH_CONFIGS[parsed.type];
|
|
132
|
+
if (!config) {
|
|
133
|
+
throw new Error(`No authentication config found for environment type: ${parsed.type}`);
|
|
134
|
+
}
|
|
135
|
+
return config;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Generate PKCE code verifier (random string)
|
|
139
|
+
* RFC 7636: 43-128 characters, unreserved characters [A-Z] [a-z] [0-9] - . _ ~
|
|
140
|
+
*/
|
|
141
|
+
function generateCodeVerifier() {
|
|
142
|
+
const buffer = (0, crypto_1.randomBytes)(32);
|
|
143
|
+
return base64URLEncode(buffer);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Generate PKCE code challenge from verifier
|
|
147
|
+
* RFC 7636: BASE64URL(SHA256(ASCII(code_verifier)))
|
|
148
|
+
*/
|
|
149
|
+
function generateCodeChallenge(verifier) {
|
|
150
|
+
const hash = (0, crypto_1.createHash)('sha256').update(verifier).digest();
|
|
151
|
+
return base64URLEncode(hash);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Base64 URL-safe encoding (no padding)
|
|
155
|
+
*/
|
|
156
|
+
function base64URLEncode(buffer) {
|
|
157
|
+
return buffer.toString('base64')
|
|
158
|
+
.replace(/\+/g, '-')
|
|
159
|
+
.replace(/\//g, '_')
|
|
160
|
+
.replace(/=/g, '');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Generate PKCE challenge pair
|
|
164
|
+
*/
|
|
165
|
+
function generatePKCEChallenge() {
|
|
166
|
+
const verifier = generateCodeVerifier();
|
|
167
|
+
const challenge = generateCodeChallenge(verifier);
|
|
168
|
+
return { verifier, challenge };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Generate random state parameter for CSRF protection
|
|
172
|
+
*/
|
|
173
|
+
function generateState() {
|
|
174
|
+
return base64URLEncode((0, crypto_1.randomBytes)(32));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Build OAuth authorization URL
|
|
178
|
+
*/
|
|
179
|
+
function buildAuthorizationUrl(config, redirectUri, codeChallenge, state, auth0OrganizationId) {
|
|
180
|
+
const params = new URLSearchParams({
|
|
181
|
+
response_type: 'code',
|
|
182
|
+
client_id: config.clientId,
|
|
183
|
+
redirect_uri: redirectUri,
|
|
184
|
+
code_challenge: codeChallenge,
|
|
185
|
+
code_challenge_method: 'S256',
|
|
186
|
+
state: state,
|
|
187
|
+
audience: config.audience,
|
|
188
|
+
scope: 'openid profile email read:current_user update:current_user read:connections write:connections read:maps write:maps read:account admin:account',
|
|
189
|
+
prompt: 'login' // Force credential entry every time
|
|
190
|
+
});
|
|
191
|
+
// Add Auth0 organization parameter if provided (for SSO login)
|
|
192
|
+
// Note: this is Auth0's organization ID, not CARTO's organization ID (ac_xxxxx)
|
|
193
|
+
if (auth0OrganizationId) {
|
|
194
|
+
params.append('organization', auth0OrganizationId);
|
|
195
|
+
}
|
|
196
|
+
return `https://${config.domain}/authorize?${params.toString()}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get Auth0 organization ID by CARTO organization name
|
|
200
|
+
* Returns null if organization doesn't have SSO configured
|
|
201
|
+
*/
|
|
202
|
+
async function getAuth0OrganizationIdByName(accountsUrl, organizationName) {
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const url = new URL(`${accountsUrl}/accounts/${encodeURIComponent(organizationName)}/auth0_org_id`);
|
|
205
|
+
const request = (0, https_1.request)({
|
|
206
|
+
hostname: url.hostname,
|
|
207
|
+
path: url.pathname,
|
|
208
|
+
headers: { 'Accept': 'application/json' }
|
|
209
|
+
}, (res) => {
|
|
210
|
+
let data = '';
|
|
211
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
212
|
+
res.on('end', () => {
|
|
213
|
+
if (res.statusCode !== 200) {
|
|
214
|
+
reject(new Error(`Organization not found: ${organizationName}`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const parsed = JSON.parse(data);
|
|
219
|
+
resolve(parsed.auth0orgId || null);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
reject(new Error(`Failed to parse response: ${err.message}`));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
request.on('error', (err) => {
|
|
227
|
+
reject(new Error(`Failed to fetch organization: ${err.message}`));
|
|
228
|
+
});
|
|
229
|
+
request.end();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Exchange authorization code for access token
|
|
234
|
+
*/
|
|
235
|
+
async function exchangeCodeForToken(config, code, codeVerifier, redirectUri) {
|
|
236
|
+
const tokenEndpoint = `https://${config.domain}/oauth/token`;
|
|
237
|
+
const body = new URLSearchParams({
|
|
238
|
+
grant_type: 'authorization_code',
|
|
239
|
+
client_id: config.clientId,
|
|
240
|
+
code: code,
|
|
241
|
+
code_verifier: codeVerifier,
|
|
242
|
+
redirect_uri: redirectUri
|
|
243
|
+
}).toString();
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const url = new URL(tokenEndpoint);
|
|
246
|
+
const options = {
|
|
247
|
+
hostname: url.hostname,
|
|
248
|
+
port: url.port,
|
|
249
|
+
path: url.pathname,
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: {
|
|
252
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
253
|
+
'Content-Length': Buffer.byteLength(body)
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const req = (0, https_1.request)(options, (res) => {
|
|
257
|
+
let data = '';
|
|
258
|
+
res.on('data', (chunk) => {
|
|
259
|
+
data += chunk;
|
|
260
|
+
});
|
|
261
|
+
res.on('end', () => {
|
|
262
|
+
try {
|
|
263
|
+
const result = JSON.parse(data);
|
|
264
|
+
if (res.statusCode === 200) {
|
|
265
|
+
resolve(result);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
reject(new Error(result.error_description || result.error || 'Token exchange failed'));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
reject(new Error('Failed to parse token response: ' + data));
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
req.on('error', (err) => {
|
|
277
|
+
reject(err);
|
|
278
|
+
});
|
|
279
|
+
req.write(body);
|
|
280
|
+
req.end();
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Extract tenant info from ID token (JWT)
|
|
285
|
+
* Note: This is a simple JWT decode without verification
|
|
286
|
+
* The token is already validated by Auth0
|
|
287
|
+
*/
|
|
288
|
+
function decodeJWT(token) {
|
|
289
|
+
try {
|
|
290
|
+
const parts = token.split('.');
|
|
291
|
+
if (parts.length !== 3) {
|
|
292
|
+
throw new Error('Invalid JWT format');
|
|
293
|
+
}
|
|
294
|
+
const payload = parts[1];
|
|
295
|
+
const decoded = Buffer.from(payload, 'base64').toString('utf-8');
|
|
296
|
+
return JSON.parse(decoded);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
throw new Error('Failed to decode JWT: ' + err.message);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Get token expiration timestamp from JWT
|
|
304
|
+
* @param token JWT token string
|
|
305
|
+
* @returns Unix timestamp (seconds) or null if not found/invalid
|
|
306
|
+
*/
|
|
307
|
+
function getTokenExpiration(token) {
|
|
308
|
+
try {
|
|
309
|
+
const payload = decodeJWT(token);
|
|
310
|
+
return payload.exp || null;
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get token issued at timestamp from JWT
|
|
318
|
+
* @param token JWT token string
|
|
319
|
+
* @returns Unix timestamp (seconds) or null if not found/invalid
|
|
320
|
+
*/
|
|
321
|
+
function getTokenIssuedAt(token) {
|
|
322
|
+
try {
|
|
323
|
+
const payload = decodeJWT(token);
|
|
324
|
+
return payload.iat || null;
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Check if token is expired
|
|
332
|
+
* @param token JWT token string
|
|
333
|
+
* @returns true if expired, false if valid or cannot determine
|
|
334
|
+
*/
|
|
335
|
+
function isTokenExpired(token) {
|
|
336
|
+
const exp = getTokenExpiration(token);
|
|
337
|
+
if (!exp)
|
|
338
|
+
return false; // Cannot determine, assume valid
|
|
339
|
+
const now = Math.floor(Date.now() / 1000);
|
|
340
|
+
return exp < now;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get token lifetime in seconds (from issued at to expiration)
|
|
344
|
+
* @param token JWT token string
|
|
345
|
+
* @returns Lifetime in seconds or null if cannot determine
|
|
346
|
+
*/
|
|
347
|
+
function getTokenLifetime(token) {
|
|
348
|
+
const exp = getTokenExpiration(token);
|
|
349
|
+
const iat = getTokenIssuedAt(token);
|
|
350
|
+
if (!exp || !iat)
|
|
351
|
+
return null;
|
|
352
|
+
return exp - iat;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Get time remaining until token expiration
|
|
356
|
+
* @param token JWT token string
|
|
357
|
+
* @returns Seconds remaining (negative if expired), or null if cannot determine
|
|
358
|
+
*/
|
|
359
|
+
function getTokenTimeRemaining(token) {
|
|
360
|
+
const exp = getTokenExpiration(token);
|
|
361
|
+
if (!exp)
|
|
362
|
+
return null;
|
|
363
|
+
const now = Math.floor(Date.now() / 1000);
|
|
364
|
+
return exp - now;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check if token should show expiration warning (< 10% of lifetime remaining)
|
|
368
|
+
* @param token JWT token string
|
|
369
|
+
* @returns true if should warn, false otherwise
|
|
370
|
+
*/
|
|
371
|
+
function shouldWarnExpiration(token) {
|
|
372
|
+
const lifetime = getTokenLifetime(token);
|
|
373
|
+
const remaining = getTokenTimeRemaining(token);
|
|
374
|
+
if (!lifetime || !remaining)
|
|
375
|
+
return false;
|
|
376
|
+
if (remaining <= 0)
|
|
377
|
+
return false; // Already expired, not just warning
|
|
378
|
+
const threshold = lifetime * 0.1; // 10% of lifetime
|
|
379
|
+
return remaining < threshold;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Format seconds into human-readable time string
|
|
383
|
+
* @param seconds Time in seconds (can be negative for expired)
|
|
384
|
+
* @returns Formatted string like "2 hours", "30 minutes", "EXPIRED 3 hours ago"
|
|
385
|
+
*/
|
|
386
|
+
function formatTimeRemaining(seconds) {
|
|
387
|
+
const absSeconds = Math.abs(seconds);
|
|
388
|
+
const isExpired = seconds < 0;
|
|
389
|
+
let timeStr;
|
|
390
|
+
if (absSeconds < 60) {
|
|
391
|
+
timeStr = `${absSeconds} second${absSeconds !== 1 ? 's' : ''}`;
|
|
392
|
+
}
|
|
393
|
+
else if (absSeconds < 3600) {
|
|
394
|
+
const mins = Math.floor(absSeconds / 60);
|
|
395
|
+
timeStr = `${mins} minute${mins !== 1 ? 's' : ''}`;
|
|
396
|
+
}
|
|
397
|
+
else if (absSeconds < 86400) {
|
|
398
|
+
const hours = Math.floor(absSeconds / 3600);
|
|
399
|
+
timeStr = `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const days = Math.floor(absSeconds / 86400);
|
|
403
|
+
timeStr = `${days} day${days !== 1 ? 's' : ''}`;
|
|
404
|
+
}
|
|
405
|
+
return isExpired ? `EXPIRED ${timeStr} ago` : timeStr;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Get M2M authentication config for the specified environment
|
|
409
|
+
*
|
|
410
|
+
* @param envOverride Optional environment override
|
|
411
|
+
*/
|
|
412
|
+
function getM2MAuthConfig(envOverride) {
|
|
413
|
+
const env = envOverride || process.env.CARTO_AUTH_ENV || exports.DEFAULT_AUTH_ENVIRONMENT;
|
|
414
|
+
const parsed = parseEnvironment(env);
|
|
415
|
+
const config = exports.M2M_AUTH_CONFIGS[parsed.type];
|
|
416
|
+
if (!config) {
|
|
417
|
+
throw new Error(`M2M authentication config not found for environment: ${parsed.type}`);
|
|
418
|
+
}
|
|
419
|
+
return config;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Exchange M2M client credentials for access token
|
|
423
|
+
* Uses OAuth2 client_credentials grant
|
|
424
|
+
*
|
|
425
|
+
* @param config M2M authentication configuration
|
|
426
|
+
* @param clientId M2M OAuth client ID
|
|
427
|
+
* @param clientSecret M2M OAuth client secret
|
|
428
|
+
* @returns Token response with access_token, expires_in, token_type
|
|
429
|
+
*/
|
|
430
|
+
async function exchangeM2MCredentialsForToken(config, clientId, clientSecret) {
|
|
431
|
+
const tokenEndpoint = `https://${config.domain}/oauth/token`;
|
|
432
|
+
// Build form-urlencoded body per CARTO docs
|
|
433
|
+
const body = new URLSearchParams({
|
|
434
|
+
grant_type: 'client_credentials',
|
|
435
|
+
client_id: clientId,
|
|
436
|
+
client_secret: clientSecret,
|
|
437
|
+
audience: config.audience
|
|
438
|
+
}).toString();
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
const url = new URL(tokenEndpoint);
|
|
441
|
+
const options = {
|
|
442
|
+
hostname: url.hostname,
|
|
443
|
+
port: url.port,
|
|
444
|
+
path: url.pathname,
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
448
|
+
'Content-Length': Buffer.byteLength(body)
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const req = (0, https_1.request)(options, (res) => {
|
|
452
|
+
let data = '';
|
|
453
|
+
res.on('data', (chunk) => {
|
|
454
|
+
data += chunk;
|
|
455
|
+
});
|
|
456
|
+
res.on('end', () => {
|
|
457
|
+
try {
|
|
458
|
+
const result = JSON.parse(data);
|
|
459
|
+
if (res.statusCode === 200) {
|
|
460
|
+
resolve(result);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
// Provide user-friendly error messages
|
|
464
|
+
let errorMessage = result.error_description || result.error || 'M2M authentication failed';
|
|
465
|
+
if (errorMessage.includes('Unauthorized')) {
|
|
466
|
+
errorMessage = 'Invalid M2M credentials. Please check your client_id and client_secret.';
|
|
467
|
+
}
|
|
468
|
+
else if (errorMessage.includes('access_denied')) {
|
|
469
|
+
errorMessage = 'Access denied. Ensure your M2M OAuth client is properly configured.';
|
|
470
|
+
}
|
|
471
|
+
reject(new Error(errorMessage));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
reject(new Error('Failed to parse M2M authentication response'));
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
req.on('error', (err) => {
|
|
480
|
+
reject(new Error(`M2M authentication request failed: ${err.message}`));
|
|
481
|
+
});
|
|
482
|
+
req.write(body);
|
|
483
|
+
req.end();
|
|
484
|
+
});
|
|
485
|
+
}
|