@heroku/heroku-fetch 0.1.6 → 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.
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>;
@@ -1,85 +0,0 @@
1
- /**
2
- * Netrc file management utilities
3
- */
4
- import { Netrc } from 'netrc-parser';
5
- import { debugCliLogin } from '../../debug-loggers.js';
6
- // Defer netrc instantiation to avoid eager file operations
7
- let _netrc;
8
- export function getNetrc() {
9
- if (!_netrc) {
10
- _netrc = new Netrc();
11
- }
12
- return _netrc;
13
- }
14
- /**
15
- * Save authentication token to netrc file
16
- */
17
- export async function saveToken(entry, apiHost, httpGitHost) {
18
- debugCliLogin('Saving credentials to netrc for hosts: %s, %s', apiHost, httpGitHost);
19
- const netrc = getNetrc();
20
- await netrc.load();
21
- const hosts = [apiHost, httpGitHost];
22
- for (const host of hosts) {
23
- if (!netrc.machines[host])
24
- netrc.machines[host] = {};
25
- netrc.machines[host].login = entry.login;
26
- netrc.machines[host].password = entry.password;
27
- delete netrc.machines[host].method;
28
- delete netrc.machines[host].org;
29
- debugCliLogin('Set credentials for host: %s (login: %s)', host, entry.login);
30
- }
31
- if (netrc.machines._tokens) {
32
- // eslint-disable-next-line unicorn/no-array-for-each
33
- netrc.machines._tokens.forEach((token) => {
34
- if (hosts.includes(token.host)) {
35
- token.internalWhitespace = '\n ';
36
- }
37
- });
38
- }
39
- await netrc.save();
40
- debugCliLogin('Netrc file saved successfully');
41
- }
42
- /**
43
- * Clear authentication tokens from netrc file
44
- */
45
- export async function clearTokens(apiHost, httpGitHost) {
46
- debugCliLogin('Clearing credentials from netrc for hosts: %s, %s', apiHost, httpGitHost);
47
- const netrc = getNetrc();
48
- await netrc.load();
49
- delete netrc.machines[apiHost];
50
- delete netrc.machines[httpGitHost];
51
- await netrc.save();
52
- debugCliLogin('Credentials cleared from netrc successfully');
53
- }
54
- /**
55
- * Get stored token from netrc file
56
- */
57
- export async function getStoredToken(apiHost) {
58
- debugCliLogin('Reading stored token from netrc for host: %s', apiHost);
59
- const netrc = getNetrc();
60
- await netrc.load();
61
- const entry = netrc.machines[apiHost];
62
- if (entry?.password) {
63
- debugCliLogin('Token found in netrc for host: %s', apiHost);
64
- }
65
- else {
66
- debugCliLogin('No token found in netrc for host: %s', apiHost);
67
- }
68
- return entry?.password;
69
- }
70
- /**
71
- * Get stored login from netrc file
72
- */
73
- export async function getStoredLogin(apiHost) {
74
- debugCliLogin('Reading stored login from netrc for host: %s', apiHost);
75
- const netrc = getNetrc();
76
- await netrc.load();
77
- const entry = netrc.machines[apiHost];
78
- if (entry?.login) {
79
- debugCliLogin('Login found in netrc for host: %s (login: %s)', apiHost, entry.login);
80
- }
81
- else {
82
- debugCliLogin('No login found in netrc for host: %s', apiHost);
83
- }
84
- return entry?.login;
85
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * OAuth token operations
3
- */
4
- import type { HerokuApiClient } from '../../client/index.js';
5
- import type { NetrcEntry } from './types.js';
6
- /**
7
- * Create an OAuth token via the Heroku API
8
- */
9
- export declare function createOAuthToken(apiUrl: string, username: string, password: string, opts?: {
10
- expiresIn?: number;
11
- secondFactor?: string;
12
- }): Promise<NetrcEntry>;
13
- /**
14
- * Get the default OAuth token for the current user
15
- */
16
- export declare function getDefaultToken(client: HerokuApiClient): Promise<string | undefined>;
17
- /**
18
- * Delete OAuth session and authorizations for the given token
19
- */
20
- export declare function deleteOAuthTokens(client: HerokuApiClient, token: string): Promise<void>;