@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.
Files changed (47) hide show
  1. package/LICENSE.txt +206 -0
  2. package/README.md +525 -0
  3. package/dist/auth/auth.d.ts +66 -0
  4. package/dist/auth/auth.js +93 -0
  5. package/dist/browser.d.ts +13 -0
  6. package/dist/browser.js +12 -0
  7. package/dist/cli/cli-login.d.ts +7 -0
  8. package/dist/cli/cli-login.js +7 -0
  9. package/dist/cli/cli-two-factor-prompt.d.ts +62 -0
  10. package/dist/cli/cli-two-factor-prompt.js +93 -0
  11. package/dist/cli/login/browser-login.d.ts +8 -0
  12. package/dist/cli/login/browser-login.js +115 -0
  13. package/dist/cli/login/index.d.ts +34 -0
  14. package/dist/cli/login/index.js +193 -0
  15. package/dist/cli/login/interactive-login.d.ts +8 -0
  16. package/dist/cli/login/interactive-login.js +59 -0
  17. package/dist/cli/login/netrc-utils.d.ts +22 -0
  18. package/dist/cli/login/netrc-utils.js +85 -0
  19. package/dist/cli/login/oauth.d.ts +20 -0
  20. package/dist/cli/login/oauth.js +142 -0
  21. package/dist/cli/login/sso-login.d.ts +8 -0
  22. package/dist/cli/login/sso-login.js +41 -0
  23. package/dist/cli/login/types.d.ts +42 -0
  24. package/dist/cli/login/types.js +11 -0
  25. package/dist/client/browser-environment-defaults.d.ts +22 -0
  26. package/dist/client/browser-environment-defaults.js +27 -0
  27. package/dist/client/http-error-handler.d.ts +10 -0
  28. package/dist/client/http-error-handler.js +44 -0
  29. package/dist/client/http-request-hooks.d.ts +18 -0
  30. package/dist/client/http-request-hooks.js +62 -0
  31. package/dist/client/index.d.ts +57 -0
  32. package/dist/client/index.js +164 -0
  33. package/dist/client/node-environment-defaults.d.ts +31 -0
  34. package/dist/client/node-environment-defaults.js +68 -0
  35. package/dist/client/service-configurations.d.ts +2 -0
  36. package/dist/client/service-configurations.js +21 -0
  37. package/dist/client/two-factor-authentication-handler.d.ts +16 -0
  38. package/dist/client/two-factor-authentication-handler.js +53 -0
  39. package/dist/debug-loggers.d.ts +6 -0
  40. package/dist/debug-loggers.js +6 -0
  41. package/dist/errors.d.ts +25 -0
  42. package/dist/errors.js +57 -0
  43. package/dist/index.d.ts +6 -0
  44. package/dist/index.js +5 -0
  45. package/dist/types.d.ts +65 -0
  46. package/dist/types.js +1 -0
  47. package/package.json +70 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CLI Login Module
3
+ *
4
+ * Re-exports the Login class from the login subfolder.
5
+ * This file maintains backward compatibility with the original import path.
6
+ */
7
+ export { Login } from './login/index.js';
@@ -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,8 @@
1
+ /**
2
+ * Browser-based OAuth login flow
3
+ */
4
+ import type { LoginConfig, NetrcEntry } from './types.js';
5
+ /**
6
+ * Perform browser-based OAuth login
7
+ */
8
+ export declare function browserLogin(config: LoginConfig, browser?: string): Promise<NetrcEntry>;
@@ -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>;