@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/README.md +19 -21
- package/dist/browser.d.ts +1 -2
- package/dist/browser.js +1 -2
- package/dist/client/http-error-handler.js +2 -1
- package/dist/client/http-request-hooks.js +2 -25
- package/dist/client/node-environment-defaults.d.ts +10 -2
- package/dist/client/node-environment-defaults.js +13 -11
- package/dist/client/two-factor-authentication-handler.js +2 -1
- package/dist/debug-loggers.d.ts +0 -1
- package/dist/debug-loggers.js +0 -1
- package/dist/host.d.ts +33 -0
- package/dist/host.js +38 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/redact.d.ts +26 -0
- package/dist/redact.js +158 -0
- package/dist/types.d.ts +8 -1
- package/package.json +3 -4
- package/dist/auth/auth.d.ts +0 -66
- package/dist/auth/auth.js +0 -93
- package/dist/cli/cli-login.d.ts +0 -7
- package/dist/cli/cli-login.js +0 -7
- package/dist/cli/login/browser-login.d.ts +0 -8
- package/dist/cli/login/browser-login.js +0 -115
- package/dist/cli/login/index.d.ts +0 -34
- package/dist/cli/login/index.js +0 -193
- package/dist/cli/login/interactive-login.d.ts +0 -8
- package/dist/cli/login/interactive-login.js +0 -59
- package/dist/cli/login/netrc-utils.d.ts +0 -22
- package/dist/cli/login/netrc-utils.js +0 -85
- package/dist/cli/login/oauth.d.ts +0 -20
- package/dist/cli/login/oauth.js +0 -142
- package/dist/cli/login/sso-login.d.ts +0 -8
- package/dist/cli/login/sso-login.js +0 -41
- package/dist/cli/login/types.d.ts +0 -42
- package/dist/cli/login/types.js +0 -11
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ npm install @heroku/heroku-fetch
|
|
|
27
27
|
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
28
28
|
|
|
29
29
|
// Create a client for the Platform API
|
|
30
|
-
// Automatically uses token from HEROKU_API_KEY env var
|
|
30
|
+
// Automatically uses token from the HEROKU_API_KEY env var
|
|
31
31
|
const client = new HerokuApiClient({
|
|
32
32
|
service: 'platform',
|
|
33
33
|
});
|
|
@@ -78,29 +78,36 @@ const customClient = new HerokuApiClient({
|
|
|
78
78
|
|
|
79
79
|
## Authentication
|
|
80
80
|
|
|
81
|
-
### Automatic Token from Environment
|
|
81
|
+
### Automatic Token from Environment (Default)
|
|
82
82
|
|
|
83
|
-
By default, the client
|
|
83
|
+
By default, the client reads the token from the `HEROKU_API_KEY` environment variable:
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
86
|
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
87
87
|
|
|
88
|
-
// Token automatically loaded from HEROKU_API_KEY
|
|
88
|
+
// Token automatically loaded from HEROKU_API_KEY
|
|
89
89
|
const client = new HerokuApiClient({
|
|
90
90
|
service: 'platform',
|
|
91
91
|
});
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
If you need to manually retrieve the token for other purposes, you can use `getAuthToken()`:
|
|
94
|
+
heroku-fetch does not read stored CLI credentials. To authenticate with
|
|
95
|
+
credentials saved by the Heroku CLI (native credential store or `~/.netrc`),
|
|
96
|
+
resolve a token with [`@heroku/heroku-credential-manager`](https://github.com/heroku/heroku-credential-manager)
|
|
97
|
+
and pass it to the client:
|
|
99
98
|
|
|
100
99
|
```typescript
|
|
101
|
-
import {
|
|
100
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
101
|
+
import { getAuth } from '@heroku/heroku-credential-manager';
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// Pass the account to read the native credential store (Keychain, libsecret,
|
|
104
|
+
// Windows Credential Manager). Passing `undefined` reads `~/.netrc` by host
|
|
105
|
+
// only — it will not find credentials stored solely in the native store.
|
|
106
|
+
const { token } = await getAuth('your-account@example.com', 'api.heroku.com');
|
|
107
|
+
const client = new HerokuApiClient({
|
|
108
|
+
service: 'platform',
|
|
109
|
+
token,
|
|
110
|
+
});
|
|
104
111
|
```
|
|
105
112
|
|
|
106
113
|
### Static Bearer Token
|
|
@@ -117,15 +124,6 @@ const client = new HerokuApiClient({
|
|
|
117
124
|
Use a function to dynamically retrieve or refresh tokens:
|
|
118
125
|
|
|
119
126
|
```typescript
|
|
120
|
-
import { getAuthTokenProvider } from '@heroku/heroku-fetch';
|
|
121
|
-
|
|
122
|
-
// Option 1: Use the built-in provider for dynamic token fetching
|
|
123
|
-
const client = new HerokuApiClient({
|
|
124
|
-
service: 'platform',
|
|
125
|
-
token: getAuthTokenProvider(), // Returns a function that fetches token each time
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Option 2: Provide your own custom token function
|
|
129
127
|
const client = new HerokuApiClient({
|
|
130
128
|
service: 'platform',
|
|
131
129
|
token: async () => {
|
|
@@ -487,7 +485,7 @@ import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
|
487
485
|
|
|
488
486
|
export default class AppsList extends Command {
|
|
489
487
|
async run() {
|
|
490
|
-
// Client automatically uses token from env
|
|
488
|
+
// Client automatically uses token from the HEROKU_API_KEY env var
|
|
491
489
|
const client = new HerokuApiClient({
|
|
492
490
|
service: 'platform',
|
|
493
491
|
});
|
package/dist/browser.d.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Browser-specific entry point for @heroku/heroku-fetch
|
|
3
3
|
*
|
|
4
4
|
* This entry point excludes Node.js-specific features like:
|
|
5
|
-
* - CLI utilities (
|
|
6
|
-
* - File system access (netrc)
|
|
5
|
+
* - CLI utilities (2FA prompts)
|
|
7
6
|
* - Process environment variables
|
|
8
7
|
*
|
|
9
8
|
* Use this entry point when bundling for browser environments.
|
package/dist/browser.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Browser-specific entry point for @heroku/heroku-fetch
|
|
3
3
|
*
|
|
4
4
|
* This entry point excludes Node.js-specific features like:
|
|
5
|
-
* - CLI utilities (
|
|
6
|
-
* - File system access (netrc)
|
|
5
|
+
* - CLI utilities (2FA prompts)
|
|
7
6
|
* - Process environment variables
|
|
8
7
|
*
|
|
9
8
|
* Use this entry point when bundling for browser environments.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { debugError } from '../debug-loggers.js';
|
|
7
7
|
import { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from '../errors.js';
|
|
8
|
+
import { logScrubbed } from '../redact.js';
|
|
8
9
|
/**
|
|
9
10
|
* Parse and handle an error response from the API
|
|
10
11
|
* Throws appropriate typed error based on status code
|
|
@@ -20,7 +21,7 @@ export async function handleErrorResponse(response) {
|
|
|
20
21
|
catch {
|
|
21
22
|
// Failed to parse error body, continue without it
|
|
22
23
|
}
|
|
23
|
-
debugError
|
|
24
|
+
await logScrubbed(debugError, 'Error response: %d %O', response.status, errorBody);
|
|
24
25
|
// Check for 2FA error (can be 403 or 412)
|
|
25
26
|
if (response.status === 412
|
|
26
27
|
|| (response.status === 403 && errorBody?.id === 'two_factor')) {
|
|
@@ -5,32 +5,9 @@
|
|
|
5
5
|
* used by the ky HTTP client.
|
|
6
6
|
*/
|
|
7
7
|
import { debugAuth, debugRequest, debugResponse } from '../debug-loggers.js';
|
|
8
|
+
import { logScrubbed } from '../redact.js';
|
|
8
9
|
import { handleErrorResponse } from './http-error-handler.js';
|
|
9
10
|
import { handle2FAChallenge, is2FAError } from './two-factor-authentication-handler.js';
|
|
10
|
-
/**
|
|
11
|
-
* Headers whose values must never appear in debug output. Compared
|
|
12
|
-
* case-insensitively. Mirrors the redaction in http-call so tokens
|
|
13
|
-
* aren't leaked when `debug` is enabled.
|
|
14
|
-
*/
|
|
15
|
-
const SENSITIVE_HEADERS = new Set([
|
|
16
|
-
'authorization',
|
|
17
|
-
'cookie',
|
|
18
|
-
'heroku-two-factor-code',
|
|
19
|
-
'proxy-authorization',
|
|
20
|
-
'x-addon-sso',
|
|
21
|
-
]);
|
|
22
|
-
/**
|
|
23
|
-
* Build a plain object of a request's headers with sensitive values
|
|
24
|
-
* replaced by `[REDACTED]`, safe for debug logging.
|
|
25
|
-
*/
|
|
26
|
-
function redactHeaders(headers) {
|
|
27
|
-
const result = {};
|
|
28
|
-
// eslint-disable-next-line unicorn/no-array-for-each
|
|
29
|
-
headers.forEach((value, key) => {
|
|
30
|
-
result[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
31
|
-
});
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
34
11
|
/**
|
|
35
12
|
* Create a beforeRequest hook that adds authentication and custom headers
|
|
36
13
|
*/
|
|
@@ -65,7 +42,7 @@ export function createBeforeRequestHook(getToken, defaultAccept, customHeaders,
|
|
|
65
42
|
}
|
|
66
43
|
debugRequest('%s %s', request.method, request.url);
|
|
67
44
|
if (debug) {
|
|
68
|
-
debugRequest
|
|
45
|
+
await logScrubbed(debugRequest, 'Headers: %O', request.headers);
|
|
69
46
|
}
|
|
70
47
|
};
|
|
71
48
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default providers for Node.js environments
|
|
3
|
-
* These use file system (netrc) and CLI utilities
|
|
4
3
|
*/
|
|
5
4
|
import type { TokenProvider, TwoFactorOptions } from '../types.js';
|
|
6
5
|
/**
|
|
7
|
-
* Get default token provider for Node.js (
|
|
6
|
+
* Get default token provider for Node.js (reads the HEROKU_API_KEY env var)
|
|
7
|
+
*
|
|
8
|
+
* heroku-fetch does not read stored credentials. To authenticate with
|
|
9
|
+
* credentials saved by the Heroku CLI, resolve a token with the
|
|
10
|
+
* heroku-credential-manager package and pass it to the client explicitly.
|
|
11
|
+
*
|
|
12
|
+
* When HEROKU_API_KEY is unset this returns `undefined`, so the request is sent
|
|
13
|
+
* with no `Authorization` header rather than failing eagerly — endpoints that
|
|
14
|
+
* need no auth still work, and endpoints that do get a server 401 surfaced as
|
|
15
|
+
* `AuthenticationError`.
|
|
8
16
|
*/
|
|
9
17
|
export declare function getDefaultTokenProvider(): TokenProvider;
|
|
10
18
|
/**
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default providers for Node.js environments
|
|
3
|
-
* These use file system (netrc) and CLI utilities
|
|
4
3
|
*/
|
|
5
4
|
import { debugRequest } from '../debug-loggers.js';
|
|
5
|
+
import { logScrubbed } from '../redact.js';
|
|
6
6
|
/**
|
|
7
|
-
* Get default token provider for Node.js (
|
|
7
|
+
* Get default token provider for Node.js (reads the HEROKU_API_KEY env var)
|
|
8
|
+
*
|
|
9
|
+
* heroku-fetch does not read stored credentials. To authenticate with
|
|
10
|
+
* credentials saved by the Heroku CLI, resolve a token with the
|
|
11
|
+
* heroku-credential-manager package and pass it to the client explicitly.
|
|
12
|
+
*
|
|
13
|
+
* When HEROKU_API_KEY is unset this returns `undefined`, so the request is sent
|
|
14
|
+
* with no `Authorization` header rather than failing eagerly — endpoints that
|
|
15
|
+
* need no auth still work, and endpoints that do get a server 401 surfaced as
|
|
16
|
+
* `AuthenticationError`.
|
|
8
17
|
*/
|
|
9
18
|
export function getDefaultTokenProvider() {
|
|
10
|
-
return
|
|
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
|
-
};
|
|
19
|
+
return () => process.env.HEROKU_API_KEY;
|
|
18
20
|
}
|
|
19
21
|
/**
|
|
20
22
|
* Get default 2FA handler for Node.js (uses CLI prompts)
|
|
@@ -56,7 +58,7 @@ export async function getDefaultDispatcher() {
|
|
|
56
58
|
return new EnvHttpProxyAgent();
|
|
57
59
|
}
|
|
58
60
|
catch (error) {
|
|
59
|
-
debugRequest
|
|
61
|
+
await logScrubbed(debugRequest, 'undici not available; skipping dispatcher (%o)', error);
|
|
60
62
|
return undefined;
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import ky from 'ky';
|
|
7
7
|
import { debugAuth, debugError } from '../debug-loggers.js';
|
|
8
|
+
import { logScrubbed } from '../redact.js';
|
|
8
9
|
/**
|
|
9
10
|
* Check if a 403 response is a 2FA error by checking the error id
|
|
10
11
|
*/
|
|
@@ -47,7 +48,7 @@ export async function handle2FAChallenge(request, twoFactorOptions, twoFactorAtt
|
|
|
47
48
|
}
|
|
48
49
|
catch (error) {
|
|
49
50
|
twoFactorAttemptedRef.value = false;
|
|
50
|
-
debugError
|
|
51
|
+
await logScrubbed(debugError, '2FA retry failed: %O', error);
|
|
51
52
|
throw error;
|
|
52
53
|
}
|
|
53
54
|
}
|
package/dist/debug-loggers.d.ts
CHANGED
package/dist/debug-loggers.js
CHANGED
|
@@ -3,4 +3,3 @@ export const debugRequest = debug('heroku:fetch:request');
|
|
|
3
3
|
export const debugResponse = debug('heroku:fetch:response');
|
|
4
4
|
export const debugAuth = debug('heroku:fetch:auth');
|
|
5
5
|
export const debugError = debug('heroku:fetch:error');
|
|
6
|
-
export const debugCliLogin = debug('heroku:fetch:cli:login');
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API host/URL utilities for the Heroku API — resolve the API host and base
|
|
3
|
+
* URL from `HEROKU_HOST`, falling back to the public defaults.
|
|
4
|
+
*
|
|
5
|
+
* Note: heroku-fetch does not read stored credentials. Credential storage and
|
|
6
|
+
* retrieval (netrc, native credential stores) lives in the
|
|
7
|
+
* `@heroku/heroku-credential-manager` package; supply a token to the client
|
|
8
|
+
* explicitly.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Get the API host from environment or default
|
|
12
|
+
*
|
|
13
|
+
* @returns The API host (default: 'api.heroku.com')
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const host = getApiHost();
|
|
18
|
+
* // Returns 'api.heroku.com' or value of HEROKU_HOST env var
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function getApiHost(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Get the full API URL from environment or default
|
|
24
|
+
*
|
|
25
|
+
* @returns The API URL (default: 'https://api.heroku.com')
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const url = getApiUrl();
|
|
30
|
+
* // Returns 'https://api.heroku.com' or https://{HEROKU_HOST}
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function getApiUrl(): string;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API host/URL utilities for the Heroku API — resolve the API host and base
|
|
3
|
+
* URL from `HEROKU_HOST`, falling back to the public defaults.
|
|
4
|
+
*
|
|
5
|
+
* Note: heroku-fetch does not read stored credentials. Credential storage and
|
|
6
|
+
* retrieval (netrc, native credential stores) lives in the
|
|
7
|
+
* `@heroku/heroku-credential-manager` package; supply a token to the client
|
|
8
|
+
* explicitly.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Get the API host from environment or default
|
|
12
|
+
*
|
|
13
|
+
* @returns The API host (default: 'api.heroku.com')
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const host = getApiHost();
|
|
18
|
+
* // Returns 'api.heroku.com' or value of HEROKU_HOST env var
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function getApiHost() {
|
|
22
|
+
return process.env.HEROKU_HOST || 'api.heroku.com';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the full API URL from environment or default
|
|
26
|
+
*
|
|
27
|
+
* @returns The API URL (default: 'https://api.heroku.com')
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const url = getApiUrl();
|
|
32
|
+
* // Returns 'https://api.heroku.com' or https://{HEROKU_HOST}
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function getApiUrl() {
|
|
36
|
+
const host = getApiHost();
|
|
37
|
+
return `https://${host}`;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { getApiHost, getApiUrl, getAuthToken, getAuthTokenProvider, } from './auth/auth.js';
|
|
2
|
-
export { Login } from './cli/cli-login.js';
|
|
3
1
|
export { cliTwoFactorPrompt, createCliTwoFactorPrompt, } from './cli/cli-two-factor-prompt.js';
|
|
4
2
|
export { HerokuApiClient } from './client/index.js';
|
|
5
3
|
export { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from './errors.js';
|
|
4
|
+
export { getApiHost, getApiUrl, } from './host.js';
|
|
6
5
|
export type { HerokuApiClientOptions, HerokuError, HerokuErrorResponse, HerokuService, RequestOptions, ServiceConfig, TokenProvider, TwoFactorOptions, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
export { getApiHost, getApiUrl, getAuthToken, getAuthTokenProvider, } from './auth/auth.js';
|
|
2
|
-
export { Login } from './cli/cli-login.js';
|
|
3
1
|
export { cliTwoFactorPrompt, createCliTwoFactorPrompt, } from './cli/cli-two-factor-prompt.js';
|
|
4
2
|
export { HerokuApiClient } from './client/index.js';
|
|
5
3
|
export { AuthenticationError, HerokuApiError, NotFoundError, RateLimitError, TwoFactorRequiredError, } from './errors.js';
|
|
4
|
+
export { getApiHost, getApiUrl, } from './host.js';
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, fail-closed secret scrubbing for DEBUG-gated log output.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@heroku/js-blanket`, loaded via dynamic `import()` the first time a
|
|
5
|
+
* debug namespace is enabled so it stays off the default code path.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Scrub a value for debug logging. Handles `Error`, `Headers`, plain objects,
|
|
9
|
+
* and primitives (passed through unchanged — a top-level string is not
|
|
10
|
+
* pattern-scrubbed; `VALUE_PATTERNS` only apply to strings nested inside a
|
|
11
|
+
* scrubbed object/Error). Fail-closed: any failure returns `'[unscrubbable]'`
|
|
12
|
+
* rather than risking the raw value. `depth` is internal, for `cause`
|
|
13
|
+
* recursion.
|
|
14
|
+
*/
|
|
15
|
+
export declare function scrubForLog(value: unknown, depth?: number): Promise<unknown>;
|
|
16
|
+
/** A `debug` logger: a format call plus an `enabled` flag for its namespace. */
|
|
17
|
+
export interface DebugLike {
|
|
18
|
+
(formatter: string, ...args: unknown[]): void;
|
|
19
|
+
readonly enabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Scrub each `value` and emit it through `logger` under `formatter`. No-ops
|
|
23
|
+
* when the namespace is disabled. Use this instead of passing raw objects to a
|
|
24
|
+
* `debug` logger with `%O`/`%o` (enforced by lint).
|
|
25
|
+
*/
|
|
26
|
+
export declare function logScrubbed(logger: DebugLike, formatter: string, ...values: unknown[]): Promise<void>;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy, fail-closed secret scrubbing for DEBUG-gated log output.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@heroku/js-blanket`, loaded via dynamic `import()` the first time a
|
|
5
|
+
* debug namespace is enabled so it stays off the default code path.
|
|
6
|
+
*/
|
|
7
|
+
/** Replacement token for redacted values. */
|
|
8
|
+
const REPLACEMENT = '[REDACTED]';
|
|
9
|
+
/**
|
|
10
|
+
* Extra sensitive keys the preset doesn't cover, matched case-insensitively as
|
|
11
|
+
* substrings. `code` is deliberately excluded so it doesn't also redact
|
|
12
|
+
* `statusCode`/`errorCode` in logged error objects.
|
|
13
|
+
*/
|
|
14
|
+
const EXTRA_FIELDS = [
|
|
15
|
+
'secondfactor',
|
|
16
|
+
'second_factor',
|
|
17
|
+
'otp',
|
|
18
|
+
'sso',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Redact inline `Bearer`/`Basic` credentials in string leaf values reached
|
|
22
|
+
* while scrubbing an object or Error (e.g. an error message). Bare UUIDs/JWTs
|
|
23
|
+
* are intentionally not matched — they're indistinguishable from benign
|
|
24
|
+
* resource IDs. Note these patterns don't apply to a top-level string passed
|
|
25
|
+
* straight to `scrubForLog`.
|
|
26
|
+
*/
|
|
27
|
+
const VALUE_PATTERNS = [
|
|
28
|
+
/\b(?:Bearer|Basic)\s+[\w.~+/=-]+/gi,
|
|
29
|
+
];
|
|
30
|
+
/** Cap on `error.cause` recursion depth; guards against cyclic chains. */
|
|
31
|
+
const MAX_CAUSE_DEPTH = 8;
|
|
32
|
+
let scrubberPromise;
|
|
33
|
+
/** Build the shared Scrubber once, importing js-blanket lazily. */
|
|
34
|
+
async function getScrubber() {
|
|
35
|
+
if (!scrubberPromise) {
|
|
36
|
+
scrubberPromise = (async () => {
|
|
37
|
+
const [{ Scrubber }, { HEROKU_FIELDS }] = await Promise.all([
|
|
38
|
+
import('@heroku/js-blanket/core/scrubber'),
|
|
39
|
+
import('@heroku/js-blanket/core/presets'),
|
|
40
|
+
]);
|
|
41
|
+
return new Scrubber({
|
|
42
|
+
fields: [...HEROKU_FIELDS, ...EXTRA_FIELDS],
|
|
43
|
+
patterns: [...VALUE_PATTERNS],
|
|
44
|
+
replacement: REPLACEMENT,
|
|
45
|
+
});
|
|
46
|
+
})();
|
|
47
|
+
// Don't cache a rejected import: clear it so a later call retries rather
|
|
48
|
+
// than degrading all object logging to '[unscrubbable]' for the process.
|
|
49
|
+
scrubberPromise.catch(() => {
|
|
50
|
+
scrubberPromise = undefined;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return scrubberPromise;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Convert a `Headers` instance to a plain object. Scrubbing one directly yields
|
|
57
|
+
* `{}` (no enumerable own-properties); converting first preserves the entries.
|
|
58
|
+
*/
|
|
59
|
+
function headersToObject(headers) {
|
|
60
|
+
const out = {};
|
|
61
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
62
|
+
headers.forEach((value, key) => {
|
|
63
|
+
out[key] = value;
|
|
64
|
+
});
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Clear credentials embedded in a URL (userinfo and sensitive query values),
|
|
69
|
+
* keeping host/path. Returns the input unchanged if it isn't a parseable URL.
|
|
70
|
+
*/
|
|
71
|
+
function redactUrl(url) {
|
|
72
|
+
if (typeof url !== 'string')
|
|
73
|
+
return url;
|
|
74
|
+
try {
|
|
75
|
+
const u = new URL(url);
|
|
76
|
+
if (u.username)
|
|
77
|
+
u.username = REPLACEMENT;
|
|
78
|
+
if (u.password)
|
|
79
|
+
u.password = REPLACEMENT;
|
|
80
|
+
for (const key of u.searchParams.keys()) {
|
|
81
|
+
if (/token|secret|code|sso|key|password|auth|assertion|saml|signature|nonce|otp|credential/i.test(key)) {
|
|
82
|
+
u.searchParams.set(key, REPLACEMENT);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return u.toString();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return url;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Normalize an `Error` into a plain, scrubbable object. An Error's own
|
|
93
|
+
* `name`/`message`/`stack`/`cause` are non-enumerable and would otherwise be
|
|
94
|
+
* dropped. Nested `Request`/`Response` are summarized rather than passed
|
|
95
|
+
* through: they serialize to `{}` and a `Request` can carry live auth headers.
|
|
96
|
+
*/
|
|
97
|
+
async function scrubError(error, depth) {
|
|
98
|
+
const scrubber = await getScrubber();
|
|
99
|
+
// Index signature makes the extra own-properties `unknown` without flattening
|
|
100
|
+
// the known `message`/`name`/`stack` off `Error` (string wins over unknown).
|
|
101
|
+
const err = error;
|
|
102
|
+
const { cause, message, name, request, response, stack, ...rest } = err;
|
|
103
|
+
const normalized = {
|
|
104
|
+
message, name, stack, ...rest,
|
|
105
|
+
};
|
|
106
|
+
if (response && typeof response === 'object') {
|
|
107
|
+
const r = response;
|
|
108
|
+
normalized.response = { status: r.status, statusText: r.statusText, url: redactUrl(r.url) };
|
|
109
|
+
}
|
|
110
|
+
if (request && typeof request === 'object') {
|
|
111
|
+
const r = request;
|
|
112
|
+
normalized.request = { method: r.method, url: redactUrl(r.url) };
|
|
113
|
+
}
|
|
114
|
+
const scrubbed = scrubber.scrub(normalized).data;
|
|
115
|
+
if (cause !== undefined) {
|
|
116
|
+
scrubbed.cause = depth >= MAX_CAUSE_DEPTH
|
|
117
|
+
? '[max cause depth exceeded]'
|
|
118
|
+
: await scrubForLog(cause, depth + 1);
|
|
119
|
+
}
|
|
120
|
+
return scrubbed;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Scrub a value for debug logging. Handles `Error`, `Headers`, plain objects,
|
|
124
|
+
* and primitives (passed through unchanged — a top-level string is not
|
|
125
|
+
* pattern-scrubbed; `VALUE_PATTERNS` only apply to strings nested inside a
|
|
126
|
+
* scrubbed object/Error). Fail-closed: any failure returns `'[unscrubbable]'`
|
|
127
|
+
* rather than risking the raw value. `depth` is internal, for `cause`
|
|
128
|
+
* recursion.
|
|
129
|
+
*/
|
|
130
|
+
export async function scrubForLog(value, depth = 0) {
|
|
131
|
+
try {
|
|
132
|
+
if (value instanceof Error)
|
|
133
|
+
return await scrubError(value, depth);
|
|
134
|
+
if (typeof Headers !== 'undefined' && value instanceof Headers) {
|
|
135
|
+
const scrubber = await getScrubber();
|
|
136
|
+
return scrubber.scrub(headersToObject(value)).data;
|
|
137
|
+
}
|
|
138
|
+
if (value !== null && typeof value === 'object') {
|
|
139
|
+
const scrubber = await getScrubber();
|
|
140
|
+
return scrubber.scrub(value).data;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return '[unscrubbable]';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Scrub each `value` and emit it through `logger` under `formatter`. No-ops
|
|
150
|
+
* when the namespace is disabled. Use this instead of passing raw objects to a
|
|
151
|
+
* `debug` logger with `%O`/`%o` (enforced by lint).
|
|
152
|
+
*/
|
|
153
|
+
export async function logScrubbed(logger, formatter, ...values) {
|
|
154
|
+
if (!logger.enabled)
|
|
155
|
+
return;
|
|
156
|
+
const scrubbed = await Promise.all(values.map(value => scrubForLog(value)));
|
|
157
|
+
logger(formatter, ...scrubbed);
|
|
158
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export type HerokuService = 'custom' | 'data' | 'particleboard' | 'platform';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A bearer token, or a function resolving one at request time. A provider may
|
|
4
|
+
* return `undefined` (sync or async) to send the request with no `Authorization`
|
|
5
|
+
* header — appropriate for endpoints or hosts that need no Heroku auth. Requests
|
|
6
|
+
* to endpoints that do require auth then receive a server 401, surfaced as
|
|
7
|
+
* `AuthenticationError`.
|
|
8
|
+
*/
|
|
9
|
+
export type TokenProvider = (() => Promise<string | undefined> | string | undefined) | string;
|
|
3
10
|
export interface TwoFactorOptions {
|
|
4
11
|
/** Callback function to prompt for 2FA code */
|
|
5
12
|
onChallenge: () => Promise<string> | string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/heroku-fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A JavaScript/TypeScript API client for Heroku APIs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -50,14 +50,13 @@
|
|
|
50
50
|
},
|
|
51
51
|
"homepage": "https://github.com/heroku/heroku-fetch#readme",
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@heroku/js-blanket": "^1.2.0",
|
|
53
54
|
"debug": "^4.3.4",
|
|
54
55
|
"ky": "^2.0.2",
|
|
55
56
|
"undici": "^8.11.2"
|
|
56
57
|
},
|
|
57
58
|
"optionalDependencies": {
|
|
58
|
-
"@heroku/heroku-cli-util": "^10.8.0"
|
|
59
|
-
"netrc-parser": "^3.1.6",
|
|
60
|
-
"open": "^11.0.0"
|
|
59
|
+
"@heroku/heroku-cli-util": "^10.8.0"
|
|
61
60
|
},
|
|
62
61
|
"devDependencies": {
|
|
63
62
|
"@heroku-cli/test-utils": "^1.0.1",
|
package/dist/auth/auth.d.ts
DELETED
|
@@ -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;
|