@heroku/heroku-fetch 0.1.1-beta.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.
- package/LICENSE.txt +206 -0
- package/README.md +525 -0
- package/dist/auth/auth.d.ts +66 -0
- package/dist/auth/auth.js +93 -0
- package/dist/browser.d.ts +13 -0
- package/dist/browser.js +12 -0
- package/dist/cli/cli-login.d.ts +7 -0
- package/dist/cli/cli-login.js +7 -0
- package/dist/cli/cli-two-factor-prompt.d.ts +62 -0
- package/dist/cli/cli-two-factor-prompt.js +93 -0
- package/dist/cli/login/browser-login.d.ts +8 -0
- package/dist/cli/login/browser-login.js +115 -0
- package/dist/cli/login/index.d.ts +34 -0
- package/dist/cli/login/index.js +193 -0
- package/dist/cli/login/interactive-login.d.ts +8 -0
- package/dist/cli/login/interactive-login.js +59 -0
- package/dist/cli/login/netrc-utils.d.ts +22 -0
- package/dist/cli/login/netrc-utils.js +85 -0
- package/dist/cli/login/oauth.d.ts +20 -0
- package/dist/cli/login/oauth.js +142 -0
- package/dist/cli/login/sso-login.d.ts +8 -0
- package/dist/cli/login/sso-login.js +41 -0
- package/dist/cli/login/types.d.ts +42 -0
- package/dist/cli/login/types.js +11 -0
- package/dist/client/browser-environment-defaults.d.ts +22 -0
- package/dist/client/browser-environment-defaults.js +27 -0
- package/dist/client/http-error-handler.d.ts +10 -0
- package/dist/client/http-error-handler.js +44 -0
- package/dist/client/http-request-hooks.d.ts +18 -0
- package/dist/client/http-request-hooks.js +62 -0
- package/dist/client/index.d.ts +57 -0
- package/dist/client/index.js +164 -0
- package/dist/client/node-environment-defaults.d.ts +31 -0
- package/dist/client/node-environment-defaults.js +68 -0
- package/dist/client/service-configurations.d.ts +2 -0
- package/dist/client/service-configurations.js +21 -0
- package/dist/client/two-factor-authentication-handler.d.ts +16 -0
- package/dist/client/two-factor-authentication-handler.js +53 -0
- package/dist/debug-loggers.d.ts +6 -0
- package/dist/debug-loggers.js +6 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +57 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Two-Factor Authentication Prompt
|
|
3
|
+
*
|
|
4
|
+
* Provides a reusable 2FA challenge handler for Heroku CLIs.
|
|
5
|
+
* This module requires @heroku/heroku-cli-util to be installed.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
10
|
+
* import { cliTwoFactorPrompt } from '@heroku/heroku-fetch/cli-two-factor-prompt';
|
|
11
|
+
*
|
|
12
|
+
* const client = new HerokuApiClient({
|
|
13
|
+
* service: 'platform',
|
|
14
|
+
* twoFactor: {
|
|
15
|
+
* onChallenge: cliTwoFactorPrompt,
|
|
16
|
+
* },
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Two-factor authentication challenge handler for Heroku CLIs.
|
|
22
|
+
*
|
|
23
|
+
* This function prompts the user for their 2FA code using heroku-cli-util's hux.prompt
|
|
24
|
+
* with masked input for security.
|
|
25
|
+
*
|
|
26
|
+
* @returns Promise that resolves with the 2FA code entered by the user
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const client = new HerokuApiClient({
|
|
31
|
+
* service: 'platform',
|
|
32
|
+
* twoFactor: {
|
|
33
|
+
* onChallenge: cliTwoFactorPrompt,
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function cliTwoFactorPrompt(): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Factory function to create a custom 2FA prompt with options.
|
|
41
|
+
*
|
|
42
|
+
* @param options - Customization options for the prompt
|
|
43
|
+
* @param options.message - Custom prompt message (default: "Enter your 2FA code")
|
|
44
|
+
* @returns A function that can be used as the onChallenge callback
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* import { createCliTwoFactorPrompt } from '@heroku/heroku-fetch/cli-two-factor-prompt';
|
|
49
|
+
*
|
|
50
|
+
* const client = new HerokuApiClient({
|
|
51
|
+
* service: 'platform',
|
|
52
|
+
* twoFactor: {
|
|
53
|
+
* onChallenge: createCliTwoFactorPrompt({
|
|
54
|
+
* message: 'Enter your Heroku 2FA code',
|
|
55
|
+
* }),
|
|
56
|
+
* },
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function createCliTwoFactorPrompt(options?: {
|
|
61
|
+
message?: string;
|
|
62
|
+
}): () => Promise<string>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Two-Factor Authentication Prompt
|
|
3
|
+
*
|
|
4
|
+
* Provides a reusable 2FA challenge handler for Heroku CLIs.
|
|
5
|
+
* This module requires @heroku/heroku-cli-util to be installed.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
10
|
+
* import { cliTwoFactorPrompt } from '@heroku/heroku-fetch/cli-two-factor-prompt';
|
|
11
|
+
*
|
|
12
|
+
* const client = new HerokuApiClient({
|
|
13
|
+
* service: 'platform',
|
|
14
|
+
* twoFactor: {
|
|
15
|
+
* onChallenge: cliTwoFactorPrompt,
|
|
16
|
+
* },
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
import { prompt } from '@heroku/heroku-cli-util/hux';
|
|
21
|
+
/**
|
|
22
|
+
* Two-factor authentication challenge handler for Heroku CLIs.
|
|
23
|
+
*
|
|
24
|
+
* This function prompts the user for their 2FA code using heroku-cli-util's hux.prompt
|
|
25
|
+
* with masked input for security.
|
|
26
|
+
*
|
|
27
|
+
* @returns Promise that resolves with the 2FA code entered by the user
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const client = new HerokuApiClient({
|
|
32
|
+
* service: 'platform',
|
|
33
|
+
* twoFactor: {
|
|
34
|
+
* onChallenge: cliTwoFactorPrompt,
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export async function cliTwoFactorPrompt() {
|
|
40
|
+
// Dynamic import to avoid requiring @heroku/heroku-cli-util for non-CLI users
|
|
41
|
+
try {
|
|
42
|
+
const code = await prompt('Enter your 2FA code', {
|
|
43
|
+
type: 'password',
|
|
44
|
+
});
|
|
45
|
+
return code;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
|
49
|
+
throw new Error('cliTwoFactorPrompt requires @heroku/heroku-cli-util to be installed. '
|
|
50
|
+
+ 'Please run: npm install @heroku/heroku-cli-util');
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Factory function to create a custom 2FA prompt with options.
|
|
57
|
+
*
|
|
58
|
+
* @param options - Customization options for the prompt
|
|
59
|
+
* @param options.message - Custom prompt message (default: "Enter your 2FA code")
|
|
60
|
+
* @returns A function that can be used as the onChallenge callback
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* import { createCliTwoFactorPrompt } from '@heroku/heroku-fetch/cli-two-factor-prompt';
|
|
65
|
+
*
|
|
66
|
+
* const client = new HerokuApiClient({
|
|
67
|
+
* service: 'platform',
|
|
68
|
+
* twoFactor: {
|
|
69
|
+
* onChallenge: createCliTwoFactorPrompt({
|
|
70
|
+
* message: 'Enter your Heroku 2FA code',
|
|
71
|
+
* }),
|
|
72
|
+
* },
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function createCliTwoFactorPrompt(options) {
|
|
77
|
+
const message = options?.message || 'Enter your 2FA code';
|
|
78
|
+
return async () => {
|
|
79
|
+
try {
|
|
80
|
+
const code = await prompt(message, {
|
|
81
|
+
type: 'password',
|
|
82
|
+
});
|
|
83
|
+
return code;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.includes('Cannot find module')) {
|
|
87
|
+
throw new Error('createCliTwoFactorPrompt requires @heroku/heroku-cli-util to be installed. '
|
|
88
|
+
+ 'Please run: npm install @heroku/heroku-cli-util');
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based OAuth login flow
|
|
3
|
+
*/
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { debugCliLogin } from '../../debug-loggers.js';
|
|
6
|
+
import { headers } from './types.js';
|
|
7
|
+
const hostname = os.hostname();
|
|
8
|
+
/**
|
|
9
|
+
* Show the manual browser login URL to the user
|
|
10
|
+
*/
|
|
11
|
+
function showManualBrowserLoginUrl(url) {
|
|
12
|
+
console.warn('If browser does not open, visit:');
|
|
13
|
+
console.error(`\u001B[32m${url}\u001B[0m`); // Green color
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Poll the auth server for the access token with retry logic
|
|
17
|
+
*/
|
|
18
|
+
async function fetchAuth(loginHost, cliUrl, token, retries = 3) {
|
|
19
|
+
try {
|
|
20
|
+
debugCliLogin('Polling auth server: %s%s (retries left: %d)', loginHost, cliUrl, retries);
|
|
21
|
+
const response = await fetch(`${loginHost}${cliUrl}`, {
|
|
22
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok && retries > 0 && response.status >= 500) {
|
|
25
|
+
debugCliLogin('Auth server returned %d, retrying...', response.status);
|
|
26
|
+
return fetchAuth(loginHost, cliUrl, token, retries - 1);
|
|
27
|
+
}
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
debugCliLogin('Auth polling failed with status: %d', response.status);
|
|
30
|
+
throw new Error(`Login failed: ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
debugCliLogin('Access token received from auth server');
|
|
33
|
+
return (await response.json());
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (retries > 0
|
|
37
|
+
&& error instanceof Error
|
|
38
|
+
&& error.message.includes('500')) {
|
|
39
|
+
debugCliLogin('Caught 500 error, retrying...');
|
|
40
|
+
return fetchAuth(loginHost, cliUrl, token, retries - 1);
|
|
41
|
+
}
|
|
42
|
+
debugCliLogin('Auth polling failed: %O', error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Perform browser-based OAuth login
|
|
48
|
+
*/
|
|
49
|
+
export async function browserLogin(config, browser) {
|
|
50
|
+
debugCliLogin('Starting browser login flow');
|
|
51
|
+
debugCliLogin('Creating auth session at %s/auth', config.loginHost);
|
|
52
|
+
// Create auth session
|
|
53
|
+
const response = await fetch(`${config.loginHost}/auth`, {
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
description: `Heroku CLI login from ${hostname}`,
|
|
56
|
+
}),
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
method: 'POST',
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
debugCliLogin('Failed to create auth session: %d %s', response.status, response.statusText);
|
|
62
|
+
throw new Error(`Failed to initiate browser login: ${response.statusText}`);
|
|
63
|
+
}
|
|
64
|
+
const urls = (await response.json());
|
|
65
|
+
const url = `${config.loginHost}${urls.browser_url}`;
|
|
66
|
+
debugCliLogin('Auth session created, browser URL: %s', url);
|
|
67
|
+
console.error(`Opening browser to ${url}\n`);
|
|
68
|
+
let urlDisplayed = false;
|
|
69
|
+
const showUrl = () => {
|
|
70
|
+
if (!urlDisplayed)
|
|
71
|
+
console.warn('Cannot open browser.');
|
|
72
|
+
urlDisplayed = true;
|
|
73
|
+
};
|
|
74
|
+
showManualBrowserLoginUrl(url);
|
|
75
|
+
const open = (await import('open')).default;
|
|
76
|
+
debugCliLogin('Opening browser with %s', browser ? `browser: ${browser}` : 'default browser');
|
|
77
|
+
const cp = await open(url, {
|
|
78
|
+
wait: false,
|
|
79
|
+
...(browser ? { app: { name: browser } } : {}),
|
|
80
|
+
});
|
|
81
|
+
cp.on('error', err => {
|
|
82
|
+
debugCliLogin('Browser open error: %O', err);
|
|
83
|
+
console.warn(err);
|
|
84
|
+
showUrl();
|
|
85
|
+
});
|
|
86
|
+
if (process.env.HEROKU_TESTING_HEADLESS_LOGIN === '1')
|
|
87
|
+
showUrl();
|
|
88
|
+
cp.on('close', code => {
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
debugCliLogin('Browser process exited with non-zero code: %d', code);
|
|
91
|
+
showUrl();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
console.error('heroku: Waiting for login...');
|
|
95
|
+
const auth = await fetchAuth(config.loginHost, urls.cli_url, urls.token);
|
|
96
|
+
if (auth.error) {
|
|
97
|
+
debugCliLogin('Auth response contained error: %s', auth.error);
|
|
98
|
+
throw new Error(auth.error);
|
|
99
|
+
}
|
|
100
|
+
console.error('Logging in...');
|
|
101
|
+
debugCliLogin('Fetching account information from %s/account', config.apiUrl);
|
|
102
|
+
const accountResponse = await fetch(`${config.apiUrl}/account`, {
|
|
103
|
+
headers: headers(auth.access_token),
|
|
104
|
+
});
|
|
105
|
+
if (!accountResponse.ok) {
|
|
106
|
+
debugCliLogin('Failed to fetch account: %d %s', accountResponse.status, accountResponse.statusText);
|
|
107
|
+
throw new Error(`Failed to fetch account: ${accountResponse.statusText}`);
|
|
108
|
+
}
|
|
109
|
+
const account = (await accountResponse.json());
|
|
110
|
+
debugCliLogin('Browser login successful for user: %s', account.email);
|
|
111
|
+
return {
|
|
112
|
+
login: account.email,
|
|
113
|
+
password: auth.access_token,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Login Module
|
|
3
|
+
*
|
|
4
|
+
* Provides login/logout functionality for Heroku CLI tools.
|
|
5
|
+
* Supports browser-based, interactive (username/password), and SSO login methods.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Login } from '@heroku/heroku-fetch/cli-login';
|
|
10
|
+
* import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
11
|
+
*
|
|
12
|
+
* const client = new HerokuApiClient({ service: 'platform' });
|
|
13
|
+
* const login = new Login(client);
|
|
14
|
+
*
|
|
15
|
+
* // Browser-based login (default)
|
|
16
|
+
* await login.login({ method: 'browser' });
|
|
17
|
+
*
|
|
18
|
+
* // Interactive login with username/password
|
|
19
|
+
* await login.login({ method: 'interactive' });
|
|
20
|
+
*
|
|
21
|
+
* // Logout
|
|
22
|
+
* await login.logout();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import type { HerokuApiClient } from '../../client/index.js';
|
|
26
|
+
import type { LoginOptions } from './types.js';
|
|
27
|
+
export declare class Login {
|
|
28
|
+
private readonly heroku;
|
|
29
|
+
private config;
|
|
30
|
+
constructor(heroku: HerokuApiClient);
|
|
31
|
+
login(opts?: LoginOptions): Promise<void>;
|
|
32
|
+
logout(token?: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export { type LoginOptions } from './types.js';
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Login Module
|
|
3
|
+
*
|
|
4
|
+
* Provides login/logout functionality for Heroku CLI tools.
|
|
5
|
+
* Supports browser-based, interactive (username/password), and SSO login methods.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Login } from '@heroku/heroku-fetch/cli-login';
|
|
10
|
+
* import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
11
|
+
*
|
|
12
|
+
* const client = new HerokuApiClient({ service: 'platform' });
|
|
13
|
+
* const login = new Login(client);
|
|
14
|
+
*
|
|
15
|
+
* // Browser-based login (default)
|
|
16
|
+
* await login.login({ method: 'browser' });
|
|
17
|
+
*
|
|
18
|
+
* // Interactive login with username/password
|
|
19
|
+
* await login.login({ method: 'interactive' });
|
|
20
|
+
*
|
|
21
|
+
* // Logout
|
|
22
|
+
* await login.logout();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import * as readline from 'node:readline';
|
|
26
|
+
import { debugCliLogin } from '../../debug-loggers.js';
|
|
27
|
+
import { browserLogin } from './browser-login.js';
|
|
28
|
+
import { interactiveLogin } from './interactive-login.js';
|
|
29
|
+
import { clearTokens, getNetrc, getStoredLogin, getStoredToken, saveToken, } from './netrc-utils.js';
|
|
30
|
+
import { deleteOAuthTokens } from './oauth.js';
|
|
31
|
+
import { ssoLogin } from './sso-login.js';
|
|
32
|
+
import { LOGIN_TIMEOUT, THIRTY_DAYS } from './types.js';
|
|
33
|
+
// Re-export types for convenience
|
|
34
|
+
/**
|
|
35
|
+
* Prompt user for login method selection
|
|
36
|
+
*/
|
|
37
|
+
async function promptForLoginMethod() {
|
|
38
|
+
console.error('heroku: Press any key to open up the browser to login or \'q\' to exit');
|
|
39
|
+
const rl = readline.createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stdout,
|
|
42
|
+
});
|
|
43
|
+
// Set raw mode to get immediate keypresses
|
|
44
|
+
process.stdin.setRawMode(true);
|
|
45
|
+
process.stdin.resume();
|
|
46
|
+
const key = await new Promise(resolve => {
|
|
47
|
+
process.stdin.once('data', data => {
|
|
48
|
+
const key = data.toString();
|
|
49
|
+
resolve(key);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
// Restore normal terminal settings
|
|
53
|
+
process.stdin.setRawMode(false);
|
|
54
|
+
rl.close();
|
|
55
|
+
console.error('');
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Convert keypress to login method
|
|
60
|
+
*/
|
|
61
|
+
function getLoginMethodFromPromptKey(key) {
|
|
62
|
+
if (key === '\u0003') {
|
|
63
|
+
// Ctrl+C
|
|
64
|
+
throw new Error('Login cancelled by user');
|
|
65
|
+
}
|
|
66
|
+
if (key.toLowerCase() === 'q') {
|
|
67
|
+
throw new Error('Login cancelled by user');
|
|
68
|
+
}
|
|
69
|
+
return 'browser';
|
|
70
|
+
}
|
|
71
|
+
export class Login {
|
|
72
|
+
constructor(heroku) {
|
|
73
|
+
this.heroku = heroku;
|
|
74
|
+
this.config = {
|
|
75
|
+
apiHost: 'api.heroku.com',
|
|
76
|
+
apiUrl: process.env.HEROKU_API_URL || 'https://api.heroku.com',
|
|
77
|
+
httpGitHost: 'git.heroku.com',
|
|
78
|
+
loginHost: process.env.HEROKU_LOGIN_HOST || 'https://cli-auth.heroku.com',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
async login(opts = {}) {
|
|
82
|
+
debugCliLogin('Login initiated with options: %O', opts);
|
|
83
|
+
let loggedIn = false;
|
|
84
|
+
try {
|
|
85
|
+
// timeout after 10 minutes
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
if (!loggedIn)
|
|
88
|
+
throw new Error('Login timed out');
|
|
89
|
+
}, LOGIN_TIMEOUT).unref();
|
|
90
|
+
if (process.env.HEROKU_API_KEY) {
|
|
91
|
+
debugCliLogin('Login blocked: HEROKU_API_KEY environment variable is set');
|
|
92
|
+
throw new Error('Cannot log in with HEROKU_API_KEY set');
|
|
93
|
+
}
|
|
94
|
+
if (opts.expiresIn && opts.expiresIn > THIRTY_DAYS) {
|
|
95
|
+
debugCliLogin('Login blocked: expiresIn (%d) exceeds 30 days', opts.expiresIn);
|
|
96
|
+
throw new Error('Cannot set an expiration longer than thirty days');
|
|
97
|
+
}
|
|
98
|
+
const netrc = getNetrc();
|
|
99
|
+
await netrc.load();
|
|
100
|
+
const previousEntry = netrc.machines[this.config.apiHost];
|
|
101
|
+
if (previousEntry) {
|
|
102
|
+
debugCliLogin('Found existing credentials in netrc for: %s', previousEntry.login);
|
|
103
|
+
}
|
|
104
|
+
let input = opts.method;
|
|
105
|
+
// Determine login method
|
|
106
|
+
if (input) {
|
|
107
|
+
debugCliLogin('Using specified login method: %s', input);
|
|
108
|
+
}
|
|
109
|
+
else if (opts.expiresIn) {
|
|
110
|
+
// can't use browser with --expires-in
|
|
111
|
+
input = 'interactive';
|
|
112
|
+
debugCliLogin('Selected interactive login (expiresIn specified)');
|
|
113
|
+
}
|
|
114
|
+
else if (process.env.HEROKU_LEGACY_SSO === '1') {
|
|
115
|
+
input = 'sso';
|
|
116
|
+
debugCliLogin('Selected SSO login (HEROKU_LEGACY_SSO=1)');
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const key = await promptForLoginMethod();
|
|
120
|
+
input = getLoginMethodFromPromptKey(key);
|
|
121
|
+
debugCliLogin('Selected %s login (from user prompt)', input);
|
|
122
|
+
}
|
|
123
|
+
// Logout previous session
|
|
124
|
+
try {
|
|
125
|
+
if (previousEntry && previousEntry.password) {
|
|
126
|
+
debugCliLogin('Logging out previous session');
|
|
127
|
+
await this.logout(previousEntry.password);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
debugCliLogin('Previous session logout failed: %s', message);
|
|
133
|
+
console.warn(message);
|
|
134
|
+
}
|
|
135
|
+
// Perform login based on selected method
|
|
136
|
+
let auth;
|
|
137
|
+
switch (input) {
|
|
138
|
+
case 'b':
|
|
139
|
+
case 'browser': {
|
|
140
|
+
debugCliLogin('Executing browser login');
|
|
141
|
+
auth = await browserLogin(this.config, opts.browser);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'i':
|
|
145
|
+
case 'interactive': {
|
|
146
|
+
debugCliLogin('Executing interactive login');
|
|
147
|
+
const previousLogin = await getStoredLogin(this.config.apiHost);
|
|
148
|
+
auth = await interactiveLogin(this.config, previousLogin, opts.expiresIn);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 's':
|
|
152
|
+
case 'sso': {
|
|
153
|
+
debugCliLogin('Executing SSO login');
|
|
154
|
+
auth = await ssoLogin(this.config);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
debugCliLogin('Unknown login method: %s, retrying', input);
|
|
159
|
+
return this.login(opts);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await saveToken(auth, this.config.apiHost, this.config.httpGitHost);
|
|
163
|
+
debugCliLogin('Login completed successfully for user: %s', auth.login);
|
|
164
|
+
console.error(`Logged in as ${auth.login}`);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
loggedIn = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async logout(token) {
|
|
171
|
+
debugCliLogin('Logout initiated');
|
|
172
|
+
// Get token from netrc if not provided
|
|
173
|
+
if (!token) {
|
|
174
|
+
token = await getStoredToken(this.config.apiHost);
|
|
175
|
+
}
|
|
176
|
+
if (!token) {
|
|
177
|
+
debugCliLogin('no credentials to logout');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// Delete OAuth tokens and sessions from API
|
|
181
|
+
// If this fails (e.g., token not found), we still want to clear local netrc
|
|
182
|
+
try {
|
|
183
|
+
await deleteOAuthTokens(this.heroku, token);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
debugCliLogin('Failed to delete OAuth tokens from API: %O', error);
|
|
187
|
+
// Continue to clear netrc even if API call fails
|
|
188
|
+
}
|
|
189
|
+
// Clear netrc entries
|
|
190
|
+
await clearTokens(this.config.apiHost, this.config.httpGitHost);
|
|
191
|
+
console.error('Logged out');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive username/password login flow
|
|
3
|
+
*/
|
|
4
|
+
import type { LoginConfig, NetrcEntry } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Perform interactive username/password login
|
|
7
|
+
*/
|
|
8
|
+
export declare function interactiveLogin(config: LoginConfig, previousLogin?: string, expiresIn?: number): Promise<NetrcEntry>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive username/password login flow
|
|
3
|
+
*/
|
|
4
|
+
import { prompt } from '@heroku/heroku-cli-util/hux';
|
|
5
|
+
import { debugCliLogin } from '../../debug-loggers.js';
|
|
6
|
+
import { createOAuthToken } from './oauth.js';
|
|
7
|
+
/**
|
|
8
|
+
* Perform interactive username/password login
|
|
9
|
+
*/
|
|
10
|
+
export async function interactiveLogin(config, previousLogin, expiresIn) {
|
|
11
|
+
debugCliLogin('Starting interactive login flow');
|
|
12
|
+
console.error('heroku: Enter your login credentials\n');
|
|
13
|
+
const email = await prompt('Email', {
|
|
14
|
+
default: previousLogin,
|
|
15
|
+
type: 'input',
|
|
16
|
+
});
|
|
17
|
+
const login = email;
|
|
18
|
+
debugCliLogin('Email provided: %s', login);
|
|
19
|
+
const password = await prompt('Password', {
|
|
20
|
+
type: 'password',
|
|
21
|
+
});
|
|
22
|
+
let auth;
|
|
23
|
+
try {
|
|
24
|
+
debugCliLogin('Creating OAuth token at %s', config.apiUrl);
|
|
25
|
+
auth = await createOAuthToken(config.apiUrl, login, password, { expiresIn });
|
|
26
|
+
debugCliLogin('OAuth token created successfully');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof Error) {
|
|
30
|
+
const errBody = error.body;
|
|
31
|
+
if (errBody?.id === 'device_trust_required') {
|
|
32
|
+
debugCliLogin('Device trust required error - 2FA must be enabled');
|
|
33
|
+
throw new Error('The interactive flag requires Two-Factor Authentication to be enabled on your account. Please use browser login.');
|
|
34
|
+
}
|
|
35
|
+
if (errBody?.id === 'two_factor') {
|
|
36
|
+
debugCliLogin('2FA challenge required, prompting for code');
|
|
37
|
+
const secondFactor = await prompt('Two-factor code', {
|
|
38
|
+
type: 'password',
|
|
39
|
+
});
|
|
40
|
+
debugCliLogin('Retrying OAuth token creation with 2FA code');
|
|
41
|
+
auth = await createOAuthToken(config.apiUrl, login, password, {
|
|
42
|
+
expiresIn,
|
|
43
|
+
secondFactor,
|
|
44
|
+
});
|
|
45
|
+
debugCliLogin('OAuth token created successfully with 2FA');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
debugCliLogin('OAuth token creation failed: %O', error);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
debugCliLogin('Unexpected error during OAuth token creation: %O', error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
debugCliLogin('Interactive login successful for user: %s', auth.login);
|
|
58
|
+
return auth;
|
|
59
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Netrc file management utilities
|
|
3
|
+
*/
|
|
4
|
+
import { Netrc } from 'netrc-parser';
|
|
5
|
+
import type { NetrcEntry } from './types.js';
|
|
6
|
+
export declare function getNetrc(): Netrc;
|
|
7
|
+
/**
|
|
8
|
+
* Save authentication token to netrc file
|
|
9
|
+
*/
|
|
10
|
+
export declare function saveToken(entry: NetrcEntry, apiHost: string, httpGitHost: string): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Clear authentication tokens from netrc file
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearTokens(apiHost: string, httpGitHost: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Get stored token from netrc file
|
|
17
|
+
*/
|
|
18
|
+
export declare function getStoredToken(apiHost: string): Promise<string | undefined>;
|
|
19
|
+
/**
|
|
20
|
+
* Get stored login from netrc file
|
|
21
|
+
*/
|
|
22
|
+
export declare function getStoredLogin(apiHost: string): Promise<string | undefined>;
|