@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,164 @@
|
|
|
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 { getDefaultDispatcher, getDefaultTokenProvider, getDefaultTwoFactorOptions } from '@heroku/heroku-fetch/client/environment-defaults.js';
|
|
9
|
+
import ky from 'ky';
|
|
10
|
+
import { debugAuth, debugRequest } from '../debug-loggers.js';
|
|
11
|
+
import { createAfterResponseHook, createBeforeRequestHook } from './http-request-hooks.js';
|
|
12
|
+
import { SERVICE_CONFIGS } from './service-configurations.js';
|
|
13
|
+
export class HerokuApiClient {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.clientPromise = null;
|
|
16
|
+
this.twoFactorAttempted = { value: false };
|
|
17
|
+
this.options = {
|
|
18
|
+
service: options.service || 'platform',
|
|
19
|
+
timeout: options.timeout || 30000,
|
|
20
|
+
...options,
|
|
21
|
+
};
|
|
22
|
+
// If no token provided, use default token provider
|
|
23
|
+
this.tokenProvider = options.token === undefined ? getDefaultTokenProvider() : options.token;
|
|
24
|
+
// If no twoFactor provided, use default 2FA handler
|
|
25
|
+
if (!options.twoFactor) {
|
|
26
|
+
this.options.twoFactor = getDefaultTwoFactorOptions();
|
|
27
|
+
}
|
|
28
|
+
// Determine base URL
|
|
29
|
+
const serviceConfig = SERVICE_CONFIGS[this.options.service];
|
|
30
|
+
let baseUrl = options.baseUrl || serviceConfig.baseUrl;
|
|
31
|
+
if (this.options.service === 'custom' && !options.baseUrl) {
|
|
32
|
+
throw new Error('baseUrl is required when service is "custom"');
|
|
33
|
+
}
|
|
34
|
+
// Apply region if specified (for services that support it)
|
|
35
|
+
if (options.region && this.options.service === 'data') {
|
|
36
|
+
baseUrl = `https://postgres-api-${options.region}.heroku.com`;
|
|
37
|
+
}
|
|
38
|
+
debugAuth('Initializing client for service: %s, baseUrl: %s', this.options.service, baseUrl);
|
|
39
|
+
// Stash the ky options; the actual ky instance is built lazily so
|
|
40
|
+
// we can await getDefaultDispatcher() (Node loads undici
|
|
41
|
+
// dynamically for env-var proxy support).
|
|
42
|
+
this.kyOptions = {
|
|
43
|
+
hooks: {
|
|
44
|
+
afterResponse: [
|
|
45
|
+
createAfterResponseHook(this.options.twoFactor, this.twoFactorAttempted),
|
|
46
|
+
],
|
|
47
|
+
beforeRequest: [
|
|
48
|
+
createBeforeRequestHook(() => this.getToken(), serviceConfig.defaultAccept, options.headers, this.options.debug),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
prefix: baseUrl,
|
|
52
|
+
timeout: this.options.timeout,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Make a DELETE request
|
|
57
|
+
*/
|
|
58
|
+
async delete(path, options) {
|
|
59
|
+
debugRequest('DELETE %s', path);
|
|
60
|
+
return this.request(path, { ...options, method: 'DELETE' });
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Make a GET request
|
|
64
|
+
*/
|
|
65
|
+
async get(path, options) {
|
|
66
|
+
debugRequest('GET %s', path);
|
|
67
|
+
return this.request(path, { ...options, method: 'GET' });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Make a PATCH request
|
|
71
|
+
*/
|
|
72
|
+
async patch(path, body, options) {
|
|
73
|
+
debugRequest('PATCH %s', path);
|
|
74
|
+
return this.request(path, { ...options, body, method: 'PATCH' });
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Make a POST request
|
|
78
|
+
*/
|
|
79
|
+
async post(path, body, options) {
|
|
80
|
+
debugRequest('POST %s', path);
|
|
81
|
+
return this.request(path, { ...options, body, method: 'POST' });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Make a PUT request
|
|
85
|
+
*/
|
|
86
|
+
async put(path, body, options) {
|
|
87
|
+
debugRequest('PUT %s', path);
|
|
88
|
+
return this.request(path, { ...options, body, method: 'PUT' });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set or update a client option
|
|
92
|
+
*/
|
|
93
|
+
setOption(key, value) {
|
|
94
|
+
if (key === 'token') {
|
|
95
|
+
this.tokenProvider = value;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
this.options[key] = value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a streaming request (returns the Response object for stream access)
|
|
104
|
+
*/
|
|
105
|
+
async stream(path, options) {
|
|
106
|
+
debugRequest('STREAM %s', path);
|
|
107
|
+
const kyOptions = {
|
|
108
|
+
headers: options?.headers,
|
|
109
|
+
method: 'GET',
|
|
110
|
+
searchParams: options?.searchParams,
|
|
111
|
+
signal: options?.signal,
|
|
112
|
+
timeout: options?.timeout || this.options.timeout,
|
|
113
|
+
};
|
|
114
|
+
const client = await this.getClient();
|
|
115
|
+
return client(path, kyOptions);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Lazily construct ky with an env-aware undici dispatcher (Node)
|
|
119
|
+
* or no dispatcher (browser). Cached after the first call.
|
|
120
|
+
*/
|
|
121
|
+
async getClient() {
|
|
122
|
+
if (!this.clientPromise) {
|
|
123
|
+
this.clientPromise = (async () => {
|
|
124
|
+
const dispatcher = await getDefaultDispatcher();
|
|
125
|
+
// ky's type definitions don't include `dispatcher`; it's
|
|
126
|
+
// passed through to fetch as undici expects it.
|
|
127
|
+
const opts = dispatcher
|
|
128
|
+
? { ...this.kyOptions, dispatcher }
|
|
129
|
+
: this.kyOptions;
|
|
130
|
+
return ky.create(opts);
|
|
131
|
+
})();
|
|
132
|
+
}
|
|
133
|
+
return this.clientPromise;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get the authorization token
|
|
137
|
+
*/
|
|
138
|
+
async getToken() {
|
|
139
|
+
if (!this.tokenProvider) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (typeof this.tokenProvider === 'string') {
|
|
143
|
+
return this.tokenProvider;
|
|
144
|
+
}
|
|
145
|
+
return this.tokenProvider();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Make a request with full control over options
|
|
149
|
+
*/
|
|
150
|
+
async request(path, options) {
|
|
151
|
+
const kyOptions = {
|
|
152
|
+
headers: options?.headers,
|
|
153
|
+
method: options?.method || 'GET',
|
|
154
|
+
searchParams: options?.searchParams,
|
|
155
|
+
signal: options?.signal,
|
|
156
|
+
timeout: options?.timeout || this.options.timeout,
|
|
157
|
+
};
|
|
158
|
+
if (options?.body !== undefined) {
|
|
159
|
+
kyOptions.json = options.body;
|
|
160
|
+
}
|
|
161
|
+
const client = await this.getClient();
|
|
162
|
+
return client(path, kyOptions);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default providers for Node.js environments
|
|
3
|
+
* These use file system (netrc) and CLI utilities
|
|
4
|
+
*/
|
|
5
|
+
import type { TokenProvider, TwoFactorOptions } from '../types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get default token provider for Node.js (uses netrc/env vars)
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDefaultTokenProvider(): TokenProvider;
|
|
10
|
+
/**
|
|
11
|
+
* Get default 2FA handler for Node.js (uses CLI prompts)
|
|
12
|
+
*/
|
|
13
|
+
export declare function getDefaultTwoFactorOptions(): TwoFactorOptions;
|
|
14
|
+
/**
|
|
15
|
+
* Get an undici Dispatcher that routes Node's fetch through an
|
|
16
|
+
* `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`-aware proxy agent.
|
|
17
|
+
*
|
|
18
|
+
* Returns `undefined` when no proxy env var is set, so we don't
|
|
19
|
+
* install a custom dispatcher that bypasses Node's global agent
|
|
20
|
+
* (and the `http`/`https` interceptors that test libraries
|
|
21
|
+
* intercept).
|
|
22
|
+
*
|
|
23
|
+
* Returns `undefined` when undici isn't loadable (e.g. on a non-Node
|
|
24
|
+
* runtime like Bun that exposes `process.versions.node` but doesn't
|
|
25
|
+
* ship undici); callers should skip the option in that case so ky
|
|
26
|
+
* falls back to native fetch.
|
|
27
|
+
*
|
|
28
|
+
* ky forwards the `dispatcher` option through to the underlying
|
|
29
|
+
* fetch — see https://github.com/sindresorhus/ky#proxy-support-nodejs.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getDefaultDispatcher(): Promise<undefined | unknown>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default providers for Node.js environments
|
|
3
|
+
* These use file system (netrc) and CLI utilities
|
|
4
|
+
*/
|
|
5
|
+
import { debugRequest } from '../debug-loggers.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get default token provider for Node.js (uses netrc/env vars)
|
|
8
|
+
*/
|
|
9
|
+
export function getDefaultTokenProvider() {
|
|
10
|
+
return async () => {
|
|
11
|
+
const { getAuthToken } = await import('../auth/auth.js');
|
|
12
|
+
const token = getAuthToken();
|
|
13
|
+
if (!token) {
|
|
14
|
+
throw new Error('No authentication token found. Please set HEROKU_API_KEY or run: heroku login');
|
|
15
|
+
}
|
|
16
|
+
return token;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get default 2FA handler for Node.js (uses CLI prompts)
|
|
21
|
+
*/
|
|
22
|
+
export function getDefaultTwoFactorOptions() {
|
|
23
|
+
return {
|
|
24
|
+
async onChallenge() {
|
|
25
|
+
const { cliTwoFactorPrompt } = await import('../cli/cli-two-factor-prompt.js');
|
|
26
|
+
return cliTwoFactorPrompt();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get an undici Dispatcher that routes Node's fetch through an
|
|
32
|
+
* `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`-aware proxy agent.
|
|
33
|
+
*
|
|
34
|
+
* Returns `undefined` when no proxy env var is set, so we don't
|
|
35
|
+
* install a custom dispatcher that bypasses Node's global agent
|
|
36
|
+
* (and the `http`/`https` interceptors that test libraries
|
|
37
|
+
* intercept).
|
|
38
|
+
*
|
|
39
|
+
* Returns `undefined` when undici isn't loadable (e.g. on a non-Node
|
|
40
|
+
* runtime like Bun that exposes `process.versions.node` but doesn't
|
|
41
|
+
* ship undici); callers should skip the option in that case so ky
|
|
42
|
+
* falls back to native fetch.
|
|
43
|
+
*
|
|
44
|
+
* ky forwards the `dispatcher` option through to the underlying
|
|
45
|
+
* fetch — see https://github.com/sindresorhus/ky#proxy-support-nodejs.
|
|
46
|
+
*/
|
|
47
|
+
export async function getDefaultDispatcher() {
|
|
48
|
+
if (!hasProxyEnv()) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// Dynamic import keeps undici out of browser bundles (the
|
|
53
|
+
// package.json `browser` condition resolves this whole module
|
|
54
|
+
// away in browser builds).
|
|
55
|
+
const { EnvHttpProxyAgent } = await import('undici');
|
|
56
|
+
return new EnvHttpProxyAgent();
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
debugRequest('undici not available; skipping dispatcher (%o)', error);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function hasProxyEnv() {
|
|
64
|
+
return Boolean(process.env.HTTP_PROXY
|
|
65
|
+
|| process.env.http_proxy
|
|
66
|
+
|| process.env.HTTPS_PROXY
|
|
67
|
+
|| process.env.https_proxy);
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const HEROKU_V3_ACCEPT = 'application/vnd.heroku+json; version=3';
|
|
2
|
+
export const SERVICE_CONFIGS = {
|
|
3
|
+
custom: {
|
|
4
|
+
// Must be provided by user; no Accept default — the URL may be
|
|
5
|
+
// a third-party host (logplex, busl, GitHub) that wouldn't
|
|
6
|
+
// recognize the Heroku vendor MIME type.
|
|
7
|
+
baseUrl: '',
|
|
8
|
+
},
|
|
9
|
+
data: {
|
|
10
|
+
baseUrl: 'https://postgres-api.heroku.com',
|
|
11
|
+
defaultAccept: HEROKU_V3_ACCEPT,
|
|
12
|
+
},
|
|
13
|
+
particleboard: {
|
|
14
|
+
baseUrl: 'https://particleboard.heroku.com',
|
|
15
|
+
defaultAccept: HEROKU_V3_ACCEPT,
|
|
16
|
+
},
|
|
17
|
+
platform: {
|
|
18
|
+
baseUrl: 'https://api.heroku.com',
|
|
19
|
+
defaultAccept: HEROKU_V3_ACCEPT,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-Factor Authentication Handler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles detection and processing of 2FA challenges from the API.
|
|
5
|
+
*/
|
|
6
|
+
import type { TwoFactorOptions } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Check if a 403 response is a 2FA error by checking the error id
|
|
9
|
+
*/
|
|
10
|
+
export declare function is2FAError(response: Response): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Handle a 2FA challenge by prompting for a code and retrying the request
|
|
13
|
+
*/
|
|
14
|
+
export declare function handle2FAChallenge(request: Request, twoFactorOptions: TwoFactorOptions, twoFactorAttemptedRef: {
|
|
15
|
+
value: boolean;
|
|
16
|
+
}): Promise<Response>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-Factor Authentication Handler Module
|
|
3
|
+
*
|
|
4
|
+
* Handles detection and processing of 2FA challenges from the API.
|
|
5
|
+
*/
|
|
6
|
+
import ky from 'ky';
|
|
7
|
+
import { debugAuth, debugError } from '../debug-loggers.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if a 403 response is a 2FA error by checking the error id
|
|
10
|
+
*/
|
|
11
|
+
export async function is2FAError(response) {
|
|
12
|
+
try {
|
|
13
|
+
const clonedResponse = response.clone();
|
|
14
|
+
const contentType = clonedResponse.headers.get('content-type');
|
|
15
|
+
if (contentType?.includes('application/json')) {
|
|
16
|
+
const errorBody = await clonedResponse.json();
|
|
17
|
+
return errorBody.id === 'two_factor';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Failed to parse, assume not a 2FA error
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Handle a 2FA challenge by prompting for a code and retrying the request
|
|
27
|
+
*/
|
|
28
|
+
export async function handle2FAChallenge(request, twoFactorOptions, twoFactorAttemptedRef) {
|
|
29
|
+
twoFactorAttemptedRef.value = true;
|
|
30
|
+
debugAuth('2FA challenge detected, invoking callback');
|
|
31
|
+
try {
|
|
32
|
+
const twoFactorCode = await twoFactorOptions.onChallenge();
|
|
33
|
+
debugAuth('2FA code received, retrying request');
|
|
34
|
+
// Retry the request with 2FA code
|
|
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
|
+
headers['Heroku-Two-Factor-Code'] = twoFactorCode;
|
|
41
|
+
const retryRequest = new Request(request, {
|
|
42
|
+
headers,
|
|
43
|
+
});
|
|
44
|
+
const retryResponse = await ky(retryRequest);
|
|
45
|
+
twoFactorAttemptedRef.value = false;
|
|
46
|
+
return retryResponse;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
twoFactorAttemptedRef.value = false;
|
|
50
|
+
debugError('2FA retry failed: %O', error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
export declare const debugRequest: debug.Debugger;
|
|
3
|
+
export declare const debugResponse: debug.Debugger;
|
|
4
|
+
export declare const debugAuth: debug.Debugger;
|
|
5
|
+
export declare const debugError: debug.Debugger;
|
|
6
|
+
export declare const debugCliLogin: debug.Debugger;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
export const debugRequest = debug('heroku:fetch:request');
|
|
3
|
+
export const debugResponse = debug('heroku:fetch:response');
|
|
4
|
+
export const debugAuth = debug('heroku:fetch:auth');
|
|
5
|
+
export const debugError = debug('heroku:fetch:error');
|
|
6
|
+
export const debugCliLogin = debug('heroku:fetch:cli:login');
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HerokuErrorResponse } from './types.js';
|
|
2
|
+
export declare class HerokuApiError extends Error {
|
|
3
|
+
errors?: Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}>;
|
|
7
|
+
id?: string;
|
|
8
|
+
resource?: string;
|
|
9
|
+
response?: Response;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
constructor(message: string, statusCode: number, response?: Response, errorBody?: HerokuErrorResponse);
|
|
12
|
+
}
|
|
13
|
+
export declare class TwoFactorRequiredError extends HerokuApiError {
|
|
14
|
+
constructor(response: Response, errorBody?: HerokuErrorResponse);
|
|
15
|
+
}
|
|
16
|
+
export declare class AuthenticationError extends HerokuApiError {
|
|
17
|
+
constructor(response: Response, errorBody?: HerokuErrorResponse);
|
|
18
|
+
}
|
|
19
|
+
export declare class NotFoundError extends HerokuApiError {
|
|
20
|
+
constructor(response: Response, errorBody?: HerokuErrorResponse);
|
|
21
|
+
}
|
|
22
|
+
export declare class RateLimitError extends HerokuApiError {
|
|
23
|
+
retryAfter?: number;
|
|
24
|
+
constructor(response: Response, errorBody?: HerokuErrorResponse);
|
|
25
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class HerokuApiError extends Error {
|
|
2
|
+
constructor(message, statusCode, response, errorBody) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'HerokuApiError';
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.response = response;
|
|
7
|
+
if (errorBody) {
|
|
8
|
+
this.id = errorBody.id;
|
|
9
|
+
this.errors = errorBody.errors;
|
|
10
|
+
this.resource = errorBody.resource;
|
|
11
|
+
}
|
|
12
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
13
|
+
if (Error.captureStackTrace) {
|
|
14
|
+
Error.captureStackTrace(this, HerokuApiError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class TwoFactorRequiredError extends HerokuApiError {
|
|
19
|
+
constructor(response, errorBody) {
|
|
20
|
+
super('Two-factor authentication required', response.status, response, errorBody);
|
|
21
|
+
this.name = 'TwoFactorRequiredError';
|
|
22
|
+
if (Error.captureStackTrace) {
|
|
23
|
+
Error.captureStackTrace(this, TwoFactorRequiredError);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class AuthenticationError extends HerokuApiError {
|
|
28
|
+
constructor(response, errorBody) {
|
|
29
|
+
super('Authentication failed', 401, response, errorBody);
|
|
30
|
+
this.name = 'AuthenticationError';
|
|
31
|
+
if (Error.captureStackTrace) {
|
|
32
|
+
Error.captureStackTrace(this, AuthenticationError);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class NotFoundError extends HerokuApiError {
|
|
37
|
+
constructor(response, errorBody) {
|
|
38
|
+
super('Resource not found', 404, response, errorBody);
|
|
39
|
+
this.name = 'NotFoundError';
|
|
40
|
+
if (Error.captureStackTrace) {
|
|
41
|
+
Error.captureStackTrace(this, NotFoundError);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class RateLimitError extends HerokuApiError {
|
|
46
|
+
constructor(response, errorBody) {
|
|
47
|
+
super('Rate limit exceeded', 429, response, errorBody);
|
|
48
|
+
this.name = 'RateLimitError';
|
|
49
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
50
|
+
if (retryAfter) {
|
|
51
|
+
this.retryAfter = Number.parseInt(retryAfter, 10);
|
|
52
|
+
}
|
|
53
|
+
if (Error.captureStackTrace) {
|
|
54
|
+
Error.captureStackTrace(this, RateLimitError);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { getApiHost, getApiUrl, getAuthToken, getAuthTokenProvider, } from './auth/auth.js';
|
|
2
|
+
export { Login } from './cli/cli-login.js';
|
|
3
|
+
export { cliTwoFactorPrompt, createCliTwoFactorPrompt, } from './cli/cli-two-factor-prompt.js';
|
|
4
|
+
export { HerokuApiClient } from './client/index.js';
|
|
5
|
+
export { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from './errors.js';
|
|
6
|
+
export type { HerokuApiClientOptions, HerokuError, HerokuErrorResponse, HerokuService, RequestOptions, ServiceConfig, TokenProvider, TwoFactorOptions, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { getApiHost, getApiUrl, getAuthToken, getAuthTokenProvider, } from './auth/auth.js';
|
|
2
|
+
export { Login } from './cli/cli-login.js';
|
|
3
|
+
export { cliTwoFactorPrompt, createCliTwoFactorPrompt, } from './cli/cli-two-factor-prompt.js';
|
|
4
|
+
export { HerokuApiClient } from './client/index.js';
|
|
5
|
+
export { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from './errors.js';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type HerokuService = 'custom' | 'data' | 'particleboard' | 'platform';
|
|
2
|
+
export type TokenProvider = (() => Promise<string> | string) | string;
|
|
3
|
+
export interface TwoFactorOptions {
|
|
4
|
+
/** Callback function to prompt for 2FA code */
|
|
5
|
+
onChallenge: () => Promise<string> | string;
|
|
6
|
+
}
|
|
7
|
+
export interface ServiceConfig {
|
|
8
|
+
/** Base URL for the service */
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Default Accept header to apply to outgoing requests when the
|
|
12
|
+
* caller hasn't set one. Omit for services that should not inject
|
|
13
|
+
* any Accept default — e.g. `custom`, where the URL may be a
|
|
14
|
+
* third-party host that wouldn't recognize the Heroku vendor MIME
|
|
15
|
+
* type.
|
|
16
|
+
*/
|
|
17
|
+
defaultAccept?: string;
|
|
18
|
+
/** Default region (if applicable) */
|
|
19
|
+
region?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface HerokuApiClientOptions {
|
|
22
|
+
/** Custom base URL (for 'custom' service type) */
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
/** Enable debugging */
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
/** Additional custom headers */
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
/** Service region (e.g., 'eu', 'us') */
|
|
29
|
+
region?: string;
|
|
30
|
+
/** Heroku service type (platform, data, particleboard, custom) */
|
|
31
|
+
service?: HerokuService;
|
|
32
|
+
/** Request timeout in milliseconds */
|
|
33
|
+
timeout?: number;
|
|
34
|
+
/** Static bearer token or function to retrieve token */
|
|
35
|
+
token?: TokenProvider;
|
|
36
|
+
/** Two-factor authentication configuration */
|
|
37
|
+
twoFactor?: TwoFactorOptions;
|
|
38
|
+
}
|
|
39
|
+
export interface RequestOptions {
|
|
40
|
+
/** Additional headers for this request */
|
|
41
|
+
headers?: Record<string, string>;
|
|
42
|
+
/** Query parameters */
|
|
43
|
+
searchParams?: Record<string, boolean | number | string>;
|
|
44
|
+
/** Aborts the in-flight request when the signal fires. */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
/** Enable streaming response */
|
|
47
|
+
stream?: boolean;
|
|
48
|
+
/** Request timeout override */
|
|
49
|
+
timeout?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface HerokuError {
|
|
52
|
+
id: string;
|
|
53
|
+
message: string;
|
|
54
|
+
}
|
|
55
|
+
export interface HerokuErrorResponse {
|
|
56
|
+
errors?: HerokuError[];
|
|
57
|
+
id: string;
|
|
58
|
+
message?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The kind of resource that was missing or invalid (e.g. `add_on`,
|
|
61
|
+
* `app`). Returned by the platform on certain 404/422 responses;
|
|
62
|
+
* absent on others.
|
|
63
|
+
*/
|
|
64
|
+
resource?: string;
|
|
65
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heroku/heroku-fetch",
|
|
3
|
+
"version": "0.1.1-beta.0",
|
|
4
|
+
"description": "A JavaScript/TypeScript API client for Heroku APIs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": {
|
|
11
|
+
"types": "./dist/browser.d.ts",
|
|
12
|
+
"default": "./dist/browser.js"
|
|
13
|
+
},
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client/environment-defaults.js": {
|
|
18
|
+
"browser": "./dist/client/browser-environment-defaults.js",
|
|
19
|
+
"default": "./dist/client/node-environment-defaults.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"test:coverage": "vitest run --coverage",
|
|
30
|
+
"test:coverage:report": "vitest run --coverage --coverage.reporter=html --coverage.reporter=text",
|
|
31
|
+
"lint": "eslint src",
|
|
32
|
+
"lint:fix": "eslint src --fix",
|
|
33
|
+
"prepare": "npm run build",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"heroku",
|
|
38
|
+
"api",
|
|
39
|
+
"client",
|
|
40
|
+
"fetch",
|
|
41
|
+
"typescript"
|
|
42
|
+
],
|
|
43
|
+
"author": "Heroku",
|
|
44
|
+
"license": "Apache-2.0",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"debug": "^4.3.4",
|
|
47
|
+
"ky": "^2.0.2",
|
|
48
|
+
"undici": "^6.25.0"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"@heroku/heroku-cli-util": "^10.8.0",
|
|
52
|
+
"netrc-parser": "^3.1.6",
|
|
53
|
+
"open": "^11.0.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@heroku-cli/test-utils": "^0.2.0",
|
|
57
|
+
"@oclif/core": "^4.10.5",
|
|
58
|
+
"@types/debug": "^4.1.12",
|
|
59
|
+
"@types/node": "^20.11.19",
|
|
60
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
61
|
+
"eslint": "^9.39.4",
|
|
62
|
+
"eslint-config-oclif": "^6.0.152",
|
|
63
|
+
"tsx": "^4.7.1",
|
|
64
|
+
"typescript": "^5.3.3",
|
|
65
|
+
"vitest": "^4.1.6"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22"
|
|
69
|
+
}
|
|
70
|
+
}
|