@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,85 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth token operations
|
|
3
|
+
*/
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { debugCliLogin } from '../../debug-loggers.js';
|
|
6
|
+
import { THIRTY_DAYS } from './types.js';
|
|
7
|
+
const hostname = os.hostname();
|
|
8
|
+
/**
|
|
9
|
+
* Create a basic auth header value
|
|
10
|
+
*/
|
|
11
|
+
function basicAuth(username, password) {
|
|
12
|
+
const auth = Buffer.from([username, password].join(':')).toString('base64');
|
|
13
|
+
return `Basic ${auth}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create an OAuth token via the Heroku API
|
|
17
|
+
*/
|
|
18
|
+
export async function createOAuthToken(apiUrl, username, password, opts = {}) {
|
|
19
|
+
debugCliLogin('Creating OAuth authorization for user: %s', username);
|
|
20
|
+
debugCliLogin('Expires in: %d seconds%s', opts.expiresIn || THIRTY_DAYS, opts.secondFactor ? ', with 2FA' : '');
|
|
21
|
+
const requestHeaders = {
|
|
22
|
+
Accept: 'application/vnd.heroku+json; version=3',
|
|
23
|
+
Authorization: basicAuth(username, password),
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
};
|
|
26
|
+
if (opts.secondFactor) {
|
|
27
|
+
requestHeaders['Heroku-Two-Factor-Code'] = opts.secondFactor;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(`${apiUrl}/oauth/authorizations`, {
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
description: `Heroku CLI login from ${hostname}`,
|
|
32
|
+
expires_in: opts.expiresIn || THIRTY_DAYS,
|
|
33
|
+
scope: ['global'],
|
|
34
|
+
}),
|
|
35
|
+
headers: requestHeaders,
|
|
36
|
+
method: 'POST',
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const errorBody = await response.json();
|
|
40
|
+
debugCliLogin('OAuth authorization creation failed: %d %s (error id: %s)', response.status, response.statusText, errorBody.id);
|
|
41
|
+
const error = new Error(errorBody.message || response.statusText);
|
|
42
|
+
error.statusCode = response.status;
|
|
43
|
+
error.body = errorBody;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const auth = (await response.json());
|
|
47
|
+
debugCliLogin('OAuth authorization created successfully (id: %s)', auth.id);
|
|
48
|
+
return {
|
|
49
|
+
login: auth.user.email,
|
|
50
|
+
password: auth.access_token.token,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the default OAuth token for the current user
|
|
55
|
+
*/
|
|
56
|
+
export async function getDefaultToken(client) {
|
|
57
|
+
try {
|
|
58
|
+
debugCliLogin('Fetching default OAuth authorization');
|
|
59
|
+
const response = await client.get('/oauth/authorizations/~');
|
|
60
|
+
const authorization = (await response.json());
|
|
61
|
+
debugCliLogin('Default authorization found: %s', authorization.id);
|
|
62
|
+
return authorization.access_token?.token;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof Error) {
|
|
66
|
+
const errBody = error.body;
|
|
67
|
+
if (errBody?.id === 'not_found'
|
|
68
|
+
&& errBody?.resource === 'authorization') {
|
|
69
|
+
debugCliLogin('Default authorization not found');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (errBody?.id === 'unauthorized') {
|
|
73
|
+
debugCliLogin('Unauthorized to fetch default authorization');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
debugCliLogin('Error fetching default authorization: %O', error);
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Delete OAuth session and authorizations for the given token
|
|
83
|
+
*/
|
|
84
|
+
export async function deleteOAuthTokens(client, token) {
|
|
85
|
+
debugCliLogin('Deleting OAuth tokens and sessions');
|
|
86
|
+
// Delete the OAuth session (for SSO logins)
|
|
87
|
+
const sessionDeletion = client
|
|
88
|
+
.delete('/oauth/sessions/~')
|
|
89
|
+
.then(() => {
|
|
90
|
+
debugCliLogin('OAuth session deleted successfully');
|
|
91
|
+
})
|
|
92
|
+
.catch(error => {
|
|
93
|
+
if (error instanceof Error) {
|
|
94
|
+
const errBody = error.body;
|
|
95
|
+
if (errBody?.id === 'not_found'
|
|
96
|
+
&& errBody?.resource === 'session') {
|
|
97
|
+
debugCliLogin('OAuth session not found (expected for non-SSO logins)');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (errBody?.id === 'unauthorized') {
|
|
101
|
+
debugCliLogin('Unauthorized to delete OAuth session');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
debugCliLogin('Error deleting OAuth session: %O', error);
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
// Delete OAuth authorizations matching this token
|
|
109
|
+
const authorizationsDeletion = client
|
|
110
|
+
.get('/oauth/authorizations')
|
|
111
|
+
.then(async (response) => {
|
|
112
|
+
const authorizations = (await response.json());
|
|
113
|
+
debugCliLogin('Found %d OAuth authorizations', authorizations.length);
|
|
114
|
+
// Don't delete the default token
|
|
115
|
+
const defaultToken = await getDefaultToken(client);
|
|
116
|
+
if (defaultToken === token) {
|
|
117
|
+
debugCliLogin('Token matches default authorization, skipping deletion');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const matchingAuths = authorizations.filter(a => a.access_token && a.access_token.token === token);
|
|
121
|
+
debugCliLogin('Found %d authorizations matching token', matchingAuths.length);
|
|
122
|
+
if (matchingAuths.length > 0) {
|
|
123
|
+
return Promise.all(matchingAuths.map(a => {
|
|
124
|
+
debugCliLogin('Deleting OAuth authorization: %s', a.id);
|
|
125
|
+
return client.delete(`/oauth/authorizations/${a.id}`);
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
.catch(error => {
|
|
130
|
+
if (error instanceof Error) {
|
|
131
|
+
const errBody = error.body;
|
|
132
|
+
if (errBody?.id === 'unauthorized') {
|
|
133
|
+
debugCliLogin('Unauthorized to list/delete OAuth authorizations');
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
debugCliLogin('Error deleting OAuth authorizations: %O', error);
|
|
138
|
+
throw error;
|
|
139
|
+
});
|
|
140
|
+
await Promise.all([sessionDeletion, authorizationsDeletion]);
|
|
141
|
+
debugCliLogin('OAuth token deletion completed');
|
|
142
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSO (Single Sign-On) login flow
|
|
3
|
+
*/
|
|
4
|
+
import { prompt } from '@heroku/heroku-cli-util/hux';
|
|
5
|
+
import { debugCliLogin } from '../../debug-loggers.js';
|
|
6
|
+
import { headers } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Perform SSO login
|
|
9
|
+
*/
|
|
10
|
+
export async function ssoLogin(config) {
|
|
11
|
+
const open = (await import('open')).default;
|
|
12
|
+
let url = process.env.SSO_URL;
|
|
13
|
+
let org = process.env.HEROKU_ORGANIZATION;
|
|
14
|
+
if (!url) {
|
|
15
|
+
const orgName = await prompt('Organization name', {
|
|
16
|
+
default: org,
|
|
17
|
+
type: 'input',
|
|
18
|
+
});
|
|
19
|
+
org = orgName;
|
|
20
|
+
url = `https://sso.heroku.com/saml/${encodeURIComponent(org)}/init?cli=true`;
|
|
21
|
+
}
|
|
22
|
+
debugCliLogin(`opening browser to ${url}`);
|
|
23
|
+
console.error(`Opening browser to:\n${url}\n`);
|
|
24
|
+
console.error('\u001B[90mIf the browser fails to open or you\'re authenticating on a remote\nmachine, please manually open the URL above in your browser.\u001B[0m\n');
|
|
25
|
+
await open(url, { wait: false });
|
|
26
|
+
const password = await prompt('Access token', {
|
|
27
|
+
type: 'password',
|
|
28
|
+
});
|
|
29
|
+
console.error('Validating token...');
|
|
30
|
+
const response = await fetch(`${config.apiUrl}/account`, {
|
|
31
|
+
headers: headers(password),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Failed to validate token: ${response.statusText}`);
|
|
35
|
+
}
|
|
36
|
+
const account = (await response.json());
|
|
37
|
+
return {
|
|
38
|
+
login: account.email,
|
|
39
|
+
password,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and interfaces for login functionality
|
|
3
|
+
*/
|
|
4
|
+
export interface NetrcEntry {
|
|
5
|
+
login: string;
|
|
6
|
+
password: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OAuthAuthorization {
|
|
9
|
+
access_token?: {
|
|
10
|
+
token: string;
|
|
11
|
+
};
|
|
12
|
+
id: string;
|
|
13
|
+
user?: {
|
|
14
|
+
email: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface Account {
|
|
18
|
+
email: string;
|
|
19
|
+
id: string;
|
|
20
|
+
}
|
|
21
|
+
export interface LoginOptions {
|
|
22
|
+
/** Browser to use for browser-based login */
|
|
23
|
+
browser?: string;
|
|
24
|
+
/** Token expiration time in seconds */
|
|
25
|
+
expiresIn?: number;
|
|
26
|
+
/** Login method to use */
|
|
27
|
+
method?: 'browser' | 'interactive' | 'sso';
|
|
28
|
+
}
|
|
29
|
+
export interface LoginConfig {
|
|
30
|
+
apiHost: string;
|
|
31
|
+
apiUrl: string;
|
|
32
|
+
httpGitHost: string;
|
|
33
|
+
loginHost: string;
|
|
34
|
+
}
|
|
35
|
+
/** Helper to create HTTP headers with bearer token */
|
|
36
|
+
export declare const headers: (token: string) => {
|
|
37
|
+
Accept: string;
|
|
38
|
+
Authorization: string;
|
|
39
|
+
};
|
|
40
|
+
/** Constants */
|
|
41
|
+
export declare const THIRTY_DAYS: number;
|
|
42
|
+
export declare const LOGIN_TIMEOUT: number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and interfaces for login functionality
|
|
3
|
+
*/
|
|
4
|
+
/** Helper to create HTTP headers with bearer token */
|
|
5
|
+
export const headers = (token) => ({
|
|
6
|
+
Accept: 'application/vnd.heroku+json; version=3',
|
|
7
|
+
Authorization: `Bearer ${token}`,
|
|
8
|
+
});
|
|
9
|
+
/** Constants */
|
|
10
|
+
export const THIRTY_DAYS = 60 * 60 * 24 * 30;
|
|
11
|
+
export const LOGIN_TIMEOUT = 1000 * 60 * 10; // 10 minutes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default providers for browser environments
|
|
3
|
+
* These are minimal since browsers don't have file system or CLI access
|
|
4
|
+
*/
|
|
5
|
+
import type { TokenProvider, TwoFactorOptions } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get default token provider for browsers (none - must be explicitly provided)
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDefaultTokenProvider(): TokenProvider | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Get default 2FA handler for browsers (none - must be explicitly provided)
|
|
12
|
+
*/
|
|
13
|
+
export declare function getDefaultTwoFactorOptions(): TwoFactorOptions | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Get a default dispatcher for the browser.
|
|
16
|
+
*
|
|
17
|
+
* Always `undefined`. Browsers route through their own proxy stack
|
|
18
|
+
* (OS / browser settings), and the `dispatcher` option is undici-
|
|
19
|
+
* specific anyway — passing it would be ignored or could surface as
|
|
20
|
+
* a TypeError in some bundlers.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getDefaultDispatcher(): Promise<undefined>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default providers for browser environments
|
|
3
|
+
* These are minimal since browsers don't have file system or CLI access
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Get default token provider for browsers (none - must be explicitly provided)
|
|
7
|
+
*/
|
|
8
|
+
export function getDefaultTokenProvider() {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get default 2FA handler for browsers (none - must be explicitly provided)
|
|
13
|
+
*/
|
|
14
|
+
export function getDefaultTwoFactorOptions() {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get a default dispatcher for the browser.
|
|
19
|
+
*
|
|
20
|
+
* Always `undefined`. Browsers route through their own proxy stack
|
|
21
|
+
* (OS / browser settings), and the `dispatcher` option is undici-
|
|
22
|
+
* specific anyway — passing it would be ignored or could surface as
|
|
23
|
+
* a TypeError in some bundlers.
|
|
24
|
+
*/
|
|
25
|
+
export async function getDefaultDispatcher() {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing HTTP error responses and converting them to typed exceptions.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Parse and handle an error response from the API
|
|
8
|
+
* Throws appropriate typed error based on status code
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleErrorResponse(response: Response): Promise<never>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles parsing HTTP error responses and converting them to typed exceptions.
|
|
5
|
+
*/
|
|
6
|
+
import { debugError } from '../debug-loggers.js';
|
|
7
|
+
import { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from '../errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Parse and handle an error response from the API
|
|
10
|
+
* Throws appropriate typed error based on status code
|
|
11
|
+
*/
|
|
12
|
+
export async function handleErrorResponse(response) {
|
|
13
|
+
let errorBody;
|
|
14
|
+
try {
|
|
15
|
+
const contentType = response.headers.get('content-type');
|
|
16
|
+
if (contentType?.includes('application/json')) {
|
|
17
|
+
errorBody = await response.json();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Failed to parse error body, continue without it
|
|
22
|
+
}
|
|
23
|
+
debugError('Error response: %d %O', response.status, errorBody);
|
|
24
|
+
// Check for 2FA error (can be 403 or 412)
|
|
25
|
+
if (response.status === 412
|
|
26
|
+
|| (response.status === 403 && errorBody?.id === 'two_factor')) {
|
|
27
|
+
throw new TwoFactorRequiredError(response, errorBody);
|
|
28
|
+
}
|
|
29
|
+
switch (response.status) {
|
|
30
|
+
case 401: {
|
|
31
|
+
throw new AuthenticationError(response, errorBody);
|
|
32
|
+
}
|
|
33
|
+
case 404: {
|
|
34
|
+
throw new NotFoundError(response, errorBody);
|
|
35
|
+
}
|
|
36
|
+
case 429: {
|
|
37
|
+
throw new RateLimitError(response, errorBody);
|
|
38
|
+
}
|
|
39
|
+
default: {
|
|
40
|
+
const message = errorBody?.message || `HTTP ${response.status}: ${response.statusText}`;
|
|
41
|
+
throw new HerokuApiError(message, response.status, response, errorBody);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Hooks Module
|
|
3
|
+
*
|
|
4
|
+
* Factory functions for creating beforeRequest and afterResponse hooks
|
|
5
|
+
* used by the ky HTTP client.
|
|
6
|
+
*/
|
|
7
|
+
import type { AfterResponseHook, BeforeRequestHook } from 'ky';
|
|
8
|
+
import type { TwoFactorOptions } from '../types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create a beforeRequest hook that adds authentication and custom headers
|
|
11
|
+
*/
|
|
12
|
+
export declare function createBeforeRequestHook(getToken: () => Promise<string | undefined>, defaultAccept: string | undefined, customHeaders?: Record<string, string>, debug?: boolean): BeforeRequestHook;
|
|
13
|
+
/**
|
|
14
|
+
* Create an afterResponse hook that handles 2FA challenges and errors
|
|
15
|
+
*/
|
|
16
|
+
export declare function createAfterResponseHook(twoFactorOptions: TwoFactorOptions | undefined, twoFactorAttemptedRef: {
|
|
17
|
+
value: boolean;
|
|
18
|
+
}): AfterResponseHook;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Hooks Module
|
|
3
|
+
*
|
|
4
|
+
* Factory functions for creating beforeRequest and afterResponse hooks
|
|
5
|
+
* used by the ky HTTP client.
|
|
6
|
+
*/
|
|
7
|
+
import { debugAuth, debugRequest, debugResponse } from '../debug-loggers.js';
|
|
8
|
+
import { handleErrorResponse } from './http-error-handler.js';
|
|
9
|
+
import { handle2FAChallenge, is2FAError } from './two-factor-authentication-handler.js';
|
|
10
|
+
/**
|
|
11
|
+
* Create a beforeRequest hook that adds authentication and custom headers
|
|
12
|
+
*/
|
|
13
|
+
export function createBeforeRequestHook(getToken, defaultAccept, customHeaders, debug) {
|
|
14
|
+
return async ({ request }) => {
|
|
15
|
+
// Apply the service's default Accept header when the caller
|
|
16
|
+
// hasn't set one. Services that don't declare one (e.g. `custom`)
|
|
17
|
+
// skip this entirely.
|
|
18
|
+
if (defaultAccept && !request.headers.has('Accept')) {
|
|
19
|
+
request.headers.set('Accept', defaultAccept);
|
|
20
|
+
}
|
|
21
|
+
// Add custom headers from options
|
|
22
|
+
if (customHeaders) {
|
|
23
|
+
for (const [key, value] of Object.entries(customHeaders)) {
|
|
24
|
+
request.headers.set(key, value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Add authorization header
|
|
28
|
+
const token = await getToken();
|
|
29
|
+
if (token) {
|
|
30
|
+
request.headers.set('Authorization', `Bearer ${token}`);
|
|
31
|
+
debugAuth('Authorization header added');
|
|
32
|
+
}
|
|
33
|
+
debugRequest('%s %s', request.method, request.url);
|
|
34
|
+
if (debug) {
|
|
35
|
+
const headers = {};
|
|
36
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
37
|
+
request.headers.forEach((value, key) => {
|
|
38
|
+
headers[key] = value;
|
|
39
|
+
});
|
|
40
|
+
debugRequest('Headers: %O', headers);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create an afterResponse hook that handles 2FA challenges and errors
|
|
46
|
+
*/
|
|
47
|
+
export function createAfterResponseHook(twoFactorOptions, twoFactorAttemptedRef) {
|
|
48
|
+
return async ({ request, response }) => {
|
|
49
|
+
debugResponse('%s %s -> %d', request.method, request.url, response.status);
|
|
50
|
+
// Handle 2FA challenge (can be 403 or 412)
|
|
51
|
+
const is2FAChallenge = response.status === 412
|
|
52
|
+
|| (response.status === 403 && await is2FAError(response));
|
|
53
|
+
if (is2FAChallenge && twoFactorOptions && !twoFactorAttemptedRef.value) {
|
|
54
|
+
return handle2FAChallenge(request, twoFactorOptions, twoFactorAttemptedRef);
|
|
55
|
+
}
|
|
56
|
+
// Handle errors
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
await handleErrorResponse(response);
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heroku API Client
|
|
3
|
+
*
|
|
4
|
+
* Main client class for making HTTP requests to Heroku APIs.
|
|
5
|
+
* Supports multiple services (platform, data, particleboard), authentication,
|
|
6
|
+
* two-factor authentication, and comprehensive error handling.
|
|
7
|
+
*/
|
|
8
|
+
import type { HerokuApiClientOptions, RequestOptions } from '../types.js';
|
|
9
|
+
export declare class HerokuApiClient {
|
|
10
|
+
private clientPromise;
|
|
11
|
+
private kyOptions;
|
|
12
|
+
private options;
|
|
13
|
+
private tokenProvider?;
|
|
14
|
+
private twoFactorAttempted;
|
|
15
|
+
constructor(options?: HerokuApiClientOptions);
|
|
16
|
+
/**
|
|
17
|
+
* Make a DELETE request
|
|
18
|
+
*/
|
|
19
|
+
delete(path: string, options?: RequestOptions): Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Make a GET request
|
|
22
|
+
*/
|
|
23
|
+
get(path: string, options?: RequestOptions): Promise<Response>;
|
|
24
|
+
/**
|
|
25
|
+
* Make a PATCH request
|
|
26
|
+
*/
|
|
27
|
+
patch(path: string, body?: unknown, options?: RequestOptions): Promise<Response>;
|
|
28
|
+
/**
|
|
29
|
+
* Make a POST request
|
|
30
|
+
*/
|
|
31
|
+
post(path: string, body?: unknown, options?: RequestOptions): Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Make a PUT request
|
|
34
|
+
*/
|
|
35
|
+
put(path: string, body?: unknown, options?: RequestOptions): Promise<Response>;
|
|
36
|
+
/**
|
|
37
|
+
* Set or update a client option
|
|
38
|
+
*/
|
|
39
|
+
setOption<K extends keyof HerokuApiClientOptions>(key: K, value: HerokuApiClientOptions[K]): void;
|
|
40
|
+
/**
|
|
41
|
+
* Create a streaming request (returns the Response object for stream access)
|
|
42
|
+
*/
|
|
43
|
+
stream(path: string, options?: RequestOptions): Promise<Response>;
|
|
44
|
+
/**
|
|
45
|
+
* Lazily construct ky with an env-aware undici dispatcher (Node)
|
|
46
|
+
* or no dispatcher (browser). Cached after the first call.
|
|
47
|
+
*/
|
|
48
|
+
private getClient;
|
|
49
|
+
/**
|
|
50
|
+
* Get the authorization token
|
|
51
|
+
*/
|
|
52
|
+
private getToken;
|
|
53
|
+
/**
|
|
54
|
+
* Make a request with full control over options
|
|
55
|
+
*/
|
|
56
|
+
private request;
|
|
57
|
+
}
|