@heroku/heroku-fetch 0.1.5 → 0.2.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.
@@ -1,66 +0,0 @@
1
- /**
2
- * Authentication utilities for Heroku API
3
- * Handles token retrieval from environment variables and netrc file
4
- */
5
- /**
6
- * Get the Heroku API token from environment variable or netrc file
7
- *
8
- * Priority:
9
- * 1. HEROKU_API_KEY environment variable
10
- * 2. ~/.netrc file (uses api.heroku.com or HEROKU_HOST if set)
11
- *
12
- * @returns The API token or undefined if not found
13
- *
14
- * @example
15
- * ```typescript
16
- * import { getAuthToken, HerokuApiClient } from '@heroku/heroku-fetch';
17
- *
18
- * const token = getAuthToken();
19
- * const client = new HerokuApiClient({
20
- * service: 'platform',
21
- * token,
22
- * });
23
- * ```
24
- */
25
- export declare function getAuthToken(): string | undefined;
26
- /**
27
- * Get the API host from environment or default
28
- *
29
- * @returns The API host (default: 'api.heroku.com')
30
- *
31
- * @example
32
- * ```typescript
33
- * const host = getApiHost();
34
- * // Returns 'api.heroku.com' or value of HEROKU_HOST env var
35
- * ```
36
- */
37
- export declare function getApiHost(): string;
38
- /**
39
- * Get the full API URL from environment or default
40
- *
41
- * @returns The API URL (default: 'https://api.heroku.com')
42
- *
43
- * @example
44
- * ```typescript
45
- * const url = getApiUrl();
46
- * // Returns 'https://api.heroku.com' or https://{HEROKU_HOST}
47
- * ```
48
- */
49
- export declare function getApiUrl(): string;
50
- /**
51
- * Create a token provider function for use with HerokuApiClient
52
- * This is useful when you want the token to be fetched dynamically
53
- *
54
- * @returns A function that returns the current auth token
55
- *
56
- * @example
57
- * ```typescript
58
- * import { getAuthTokenProvider, HerokuApiClient } from '@heroku/heroku-fetch';
59
- *
60
- * const client = new HerokuApiClient({
61
- * service: 'platform',
62
- * token: getAuthTokenProvider(),
63
- * });
64
- * ```
65
- */
66
- export declare function getAuthTokenProvider(): () => string | undefined;
package/dist/auth/auth.js DELETED
@@ -1,93 +0,0 @@
1
- /**
2
- * Authentication utilities for Heroku API
3
- * Handles token retrieval from environment variables and netrc file
4
- */
5
- import { Netrc } from 'netrc-parser';
6
- // Defer netrc instantiation to avoid eager .netrc file operations at module load time
7
- let _netrc;
8
- function getNetrc() {
9
- if (!_netrc) {
10
- _netrc = new Netrc();
11
- }
12
- return _netrc;
13
- }
14
- /**
15
- * Get the Heroku API token from environment variable or netrc file
16
- *
17
- * Priority:
18
- * 1. HEROKU_API_KEY environment variable
19
- * 2. ~/.netrc file (uses api.heroku.com or HEROKU_HOST if set)
20
- *
21
- * @returns The API token or undefined if not found
22
- *
23
- * @example
24
- * ```typescript
25
- * import { getAuthToken, HerokuApiClient } from '@heroku/heroku-fetch';
26
- *
27
- * const token = getAuthToken();
28
- * const client = new HerokuApiClient({
29
- * service: 'platform',
30
- * token,
31
- * });
32
- * ```
33
- */
34
- export function getAuthToken() {
35
- // Check environment variable first
36
- let token = process.env.HEROKU_API_KEY;
37
- if (!token) {
38
- // Fall back to netrc
39
- const netrc = getNetrc();
40
- netrc.loadSync();
41
- const apiHost = getApiHost();
42
- token = netrc.machines[apiHost]?.password;
43
- }
44
- return token;
45
- }
46
- /**
47
- * Get the API host from environment or default
48
- *
49
- * @returns The API host (default: 'api.heroku.com')
50
- *
51
- * @example
52
- * ```typescript
53
- * const host = getApiHost();
54
- * // Returns 'api.heroku.com' or value of HEROKU_HOST env var
55
- * ```
56
- */
57
- export function getApiHost() {
58
- return process.env.HEROKU_HOST || 'api.heroku.com';
59
- }
60
- /**
61
- * Get the full API URL from environment or default
62
- *
63
- * @returns The API URL (default: 'https://api.heroku.com')
64
- *
65
- * @example
66
- * ```typescript
67
- * const url = getApiUrl();
68
- * // Returns 'https://api.heroku.com' or https://{HEROKU_HOST}
69
- * ```
70
- */
71
- export function getApiUrl() {
72
- const host = getApiHost();
73
- return `https://${host}`;
74
- }
75
- /**
76
- * Create a token provider function for use with HerokuApiClient
77
- * This is useful when you want the token to be fetched dynamically
78
- *
79
- * @returns A function that returns the current auth token
80
- *
81
- * @example
82
- * ```typescript
83
- * import { getAuthTokenProvider, HerokuApiClient } from '@heroku/heroku-fetch';
84
- *
85
- * const client = new HerokuApiClient({
86
- * service: 'platform',
87
- * token: getAuthTokenProvider(),
88
- * });
89
- * ```
90
- */
91
- export function getAuthTokenProvider() {
92
- return () => getAuthToken();
93
- }
@@ -1,7 +0,0 @@
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, type LoginOptions } from './login/index.js';
@@ -1,7 +0,0 @@
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';
@@ -1,8 +0,0 @@
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>;
@@ -1,115 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
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';
@@ -1,193 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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>;
@@ -1,59 +0,0 @@
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
- }
@@ -1,22 +0,0 @@
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>;