@heroku/sdk 0.11.2 → 0.12.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/core/create-client.d.ts +7 -2
- package/dist/core/create-client.js +5 -2
- package/dist/core/extend-resource.d.ts +52 -2
- package/dist/core/extend-resource.js +27 -0
- package/dist/core/heroku-sdk.js +44 -1
- package/dist/resources/platform/dyno/exec.d.ts +12 -2
- package/dist/resources/platform/dyno/exec.js +23 -9
- package/dist/resources/platform/dyno/index.js +3 -3
- package/dist/resources/platform/dyno/list-extended.d.ts +7 -1
- package/dist/resources/platform/dyno/list-extended.js +8 -3
- package/dist/resources/platform/dyno/run.d.ts +1 -1
- package/dist/resources/platform/dyno/run.js +3 -2
- package/dist/resources/platform/log-session/index.d.ts +1 -1
- package/dist/resources/platform/log-session/index.js +6 -10
- package/dist/resources/platform/pipeline/index.d.ts +2 -2
- package/dist/resources/platform/pipeline/index.js +2 -3
- package/dist/resources/platform/pipeline/promotion.d.ts +1 -1
- package/dist/resources/platform/pipeline/promotion.js +9 -13
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +9 -4
|
@@ -7,7 +7,7 @@ type RoutesModule = Record<string, Record<string, RouteDefinition>>;
|
|
|
7
7
|
* and `stream` are deliberately excluded — those belong on the
|
|
8
8
|
* individual route call, not on the client wrapper.
|
|
9
9
|
*/
|
|
10
|
-
export type StickyRouteOptions = Pick<RequestOptions, 'headers' | 'signal' | 'timeout'> & {
|
|
10
|
+
export type StickyRouteOptions = Pick<RequestOptions, 'headers' | 'reauthorize' | 'signal' | 'timeout'> & {
|
|
11
11
|
/**
|
|
12
12
|
* Request body applied to write calls on the derived client. Primarily
|
|
13
13
|
* for methods whose route metadata lacks `hasRequestBody` (e.g. DELETE
|
|
@@ -30,7 +30,12 @@ export type RoutesClientExtras<T> = {
|
|
|
30
30
|
* Returns a same-shaped client where each route call applies the
|
|
31
31
|
* provided options. Caller-supplied headers override the api-client's
|
|
32
32
|
* defaults; subsequent `withOptions` calls layer headers and replace
|
|
33
|
-
* signal / timeout.
|
|
33
|
+
* signal / timeout / reauthorize.
|
|
34
|
+
*
|
|
35
|
+
* Pass `reauthorize: false` to opt the derived client's calls out of the
|
|
36
|
+
* client-level 401 refresh (see {@link RequestOptions.reauthorize}) — for
|
|
37
|
+
* call sites where a 401 means something other than an expired Heroku
|
|
38
|
+
* credential and must not drive a login.
|
|
34
39
|
*
|
|
35
40
|
* The original client is not mutated.
|
|
36
41
|
*/
|
|
@@ -11,12 +11,15 @@ function mergeStickyOptions(base, next) {
|
|
|
11
11
|
if (next.headers || base?.headers) {
|
|
12
12
|
merged.headers = { ...base?.headers, ...next.headers };
|
|
13
13
|
}
|
|
14
|
-
// signal, timeout, and body are not additive — last wins,
|
|
15
|
-
// the caller actually set them. Otherwise the previous value
|
|
14
|
+
// signal, timeout, reauthorize, and body are not additive — last wins,
|
|
15
|
+
// but only if the caller actually set them. Otherwise the previous value
|
|
16
|
+
// persists.
|
|
16
17
|
if (next.signal !== undefined)
|
|
17
18
|
merged.signal = next.signal;
|
|
18
19
|
if (next.timeout !== undefined)
|
|
19
20
|
merged.timeout = next.timeout;
|
|
21
|
+
if (next.reauthorize !== undefined)
|
|
22
|
+
merged.reauthorize = next.reauthorize;
|
|
20
23
|
if (next.body !== undefined)
|
|
21
24
|
merged.body = next.body;
|
|
22
25
|
return merged;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
1
3
|
import type { DashboardBackendClient } from '../services/dashboard-backend.js';
|
|
2
4
|
import type { DataClient } from '../services/data.js';
|
|
3
5
|
import type { MetricsClient } from '../services/metrics.js';
|
|
@@ -14,10 +16,58 @@ type BaseResourceCtx = {
|
|
|
14
16
|
platform: PlatformClient;
|
|
15
17
|
repositories: RepositoriesClient;
|
|
16
18
|
};
|
|
17
|
-
export type ResourceCtx =
|
|
19
|
+
export type ResourceCtx = {
|
|
20
|
+
/**
|
|
21
|
+
* Build a raw {@link HerokuApiClient} that inherits the SDK's resolved
|
|
22
|
+
* client options for the `inheritFrom` service bundle (token,
|
|
23
|
+
* `userAgent`/headers, `signal`, `timeout`, `warn`, …), with `overrides`
|
|
24
|
+
* applied last.
|
|
25
|
+
*
|
|
26
|
+
* `inheritFrom` selects *which resolved option bundle to start from* — not
|
|
27
|
+
* the wire service; pass `service` in `overrides` to redirect the client to
|
|
28
|
+
* a different host and auth behavior. The returned client is raw, NOT the
|
|
29
|
+
* managed service client: it does not carry that client's `defaultAccept`
|
|
30
|
+
* (e.g. the platform's `version=3.sdk`) or routes proxy, so callers issue
|
|
31
|
+
* `get`/`post`/`stream` directly and set `Accept` per call.
|
|
32
|
+
*
|
|
33
|
+
* When `overrides.service === 'custom'` the inherited bundle is narrowed to
|
|
34
|
+
* identity and cancellation only (never auth/routing), so a custom stream
|
|
35
|
+
* host can't receive the Heroku token, a `reauthorize` login callback, or
|
|
36
|
+
* consumer headers. Prefer {@link makeCustomHostClient} for that case rather
|
|
37
|
+
* than assembling the overrides by hand.
|
|
38
|
+
*
|
|
39
|
+
* Optional for compatibility with existing extension contexts; prefer
|
|
40
|
+
* {@link makeCtxClient} at call sites so a context predating this stays
|
|
41
|
+
* on the pre-threading behavior.
|
|
42
|
+
*
|
|
43
|
+
* TODO(next major): make this required on ResourceCtx and drop the
|
|
44
|
+
* {@link makeCtxClient} fallback once external hand-built contexts are no
|
|
45
|
+
* longer supported.
|
|
46
|
+
*/
|
|
47
|
+
makeClient?: (inheritFrom: ServiceName, overrides?: HerokuApiClientOptions) => HerokuApiClient;
|
|
18
48
|
/** Available to SDK-provided extensions; optional for compatibility with existing extension contexts. */
|
|
19
49
|
repositoriesApi?: RepositoriesApiClient;
|
|
20
|
-
};
|
|
50
|
+
} & BaseResourceCtx;
|
|
51
|
+
/**
|
|
52
|
+
* Build a raw {@link HerokuApiClient} from a resource context, inheriting the
|
|
53
|
+
* SDK's resolved client options for the `inheritFrom` service bundle
|
|
54
|
+
* (identity, `signal`, `warn`, token, …) with `overrides` applied last. Falls
|
|
55
|
+
* back to a bare client carrying only `overrides` when the context predates
|
|
56
|
+
* {@link ResourceCtx.makeClient} (e.g. a hand-built test ctx, or a standalone
|
|
57
|
+
* call with no SDK behind it), preserving the pre-threading behavior exactly.
|
|
58
|
+
*/
|
|
59
|
+
export declare function makeCtxClient(ctx: Pick<ResourceCtx, 'makeClient'>, inheritFrom: ServiceName, overrides?: HerokuApiClientOptions): HerokuApiClient;
|
|
60
|
+
/**
|
|
61
|
+
* Build a raw {@link HerokuApiClient} for a non-Heroku-credential host — a
|
|
62
|
+
* custom stream/API host the routes-generated clients don't cover (logplex,
|
|
63
|
+
* busl, exec-manager). Inherits only the SDK's identity and cancellation from
|
|
64
|
+
* the platform option bundle; forces `service: 'custom'`, the given `baseUrl`,
|
|
65
|
+
* and an empty `token`, and drops every auth/routing option (`reauthorize`,
|
|
66
|
+
* `headers`, `hooks`, `twoFactor`, `region`, …) so none crosses to the
|
|
67
|
+
* third-party origin. `overrides` the call site explicitly sets still apply —
|
|
68
|
+
* use them for a per-host header or timeout, not to smuggle inherited auth.
|
|
69
|
+
*/
|
|
70
|
+
export declare function makeCustomHostClient(ctx: Pick<ResourceCtx, 'makeClient'>, baseUrl: string, overrides?: Omit<HerokuApiClientOptions, 'baseUrl' | 'service' | 'token'>): HerokuApiClient;
|
|
21
71
|
export type ResourceMethods = Record<string, (...args: any[]) => any>;
|
|
22
72
|
export type ResourceExtension<S extends ServiceName = ServiceName, R extends string = string, M extends ResourceMethods = ResourceMethods> = {
|
|
23
73
|
factory: (ctx: ResourceCtx) => M;
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
|
+
/**
|
|
3
|
+
* Build a raw {@link HerokuApiClient} from a resource context, inheriting the
|
|
4
|
+
* SDK's resolved client options for the `inheritFrom` service bundle
|
|
5
|
+
* (identity, `signal`, `warn`, token, …) with `overrides` applied last. Falls
|
|
6
|
+
* back to a bare client carrying only `overrides` when the context predates
|
|
7
|
+
* {@link ResourceCtx.makeClient} (e.g. a hand-built test ctx, or a standalone
|
|
8
|
+
* call with no SDK behind it), preserving the pre-threading behavior exactly.
|
|
9
|
+
*/
|
|
10
|
+
export function makeCtxClient(ctx, inheritFrom, overrides = {}) {
|
|
11
|
+
return ctx.makeClient ? ctx.makeClient(inheritFrom, overrides) : new HerokuApiClient(overrides);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build a raw {@link HerokuApiClient} for a non-Heroku-credential host — a
|
|
15
|
+
* custom stream/API host the routes-generated clients don't cover (logplex,
|
|
16
|
+
* busl, exec-manager). Inherits only the SDK's identity and cancellation from
|
|
17
|
+
* the platform option bundle; forces `service: 'custom'`, the given `baseUrl`,
|
|
18
|
+
* and an empty `token`, and drops every auth/routing option (`reauthorize`,
|
|
19
|
+
* `headers`, `hooks`, `twoFactor`, `region`, …) so none crosses to the
|
|
20
|
+
* third-party origin. `overrides` the call site explicitly sets still apply —
|
|
21
|
+
* use them for a per-host header or timeout, not to smuggle inherited auth.
|
|
22
|
+
*/
|
|
23
|
+
export function makeCustomHostClient(ctx, baseUrl, overrides = {}) {
|
|
24
|
+
return makeCtxClient(ctx, 'platform', {
|
|
25
|
+
...overrides, baseUrl, service: 'custom', token: '',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
1
28
|
export function extendResource(service, resource, factory) {
|
|
2
29
|
return { factory, resource, service };
|
|
3
30
|
}
|
package/dist/core/heroku-sdk.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
1
2
|
import { createDashboardBackendClient } from '../services/dashboard-backend.js';
|
|
2
3
|
import { createDataClient } from '../services/data.js';
|
|
3
4
|
import { createMetricsClient } from '../services/metrics.js';
|
|
@@ -5,7 +6,17 @@ import { createNotificationsClient } from '../services/notifications.js';
|
|
|
5
6
|
import { createPlatformClient } from '../services/platform.js';
|
|
6
7
|
import { createRepositoriesApiClient } from '../services/repositories-api.js';
|
|
7
8
|
import { createRepositoriesClient } from '../services/repositories.js';
|
|
9
|
+
import { SDK_VERSION } from '../version.js';
|
|
8
10
|
import { mergeExtensions } from './extensions-proxy.js';
|
|
11
|
+
/**
|
|
12
|
+
* Default `User-Agent` identity for SDK-originated traffic. {@link HerokuSDK}
|
|
13
|
+
* applies it as a fill-if-absent default on every service client (in its
|
|
14
|
+
* `#getClientOptions` merge), so requests the SDK drives stay identifiable even
|
|
15
|
+
* when the consumer passes no `userAgent`. A consumer-supplied
|
|
16
|
+
* `clientOptions.userAgent` — or a per-service override — wins. The version is
|
|
17
|
+
* stamped from package.json at build time; see `src/version.ts`.
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_USER_AGENT = { name: '@heroku/sdk', version: SDK_VERSION };
|
|
9
20
|
function partitionByService(extensions) {
|
|
10
21
|
const map = new Map();
|
|
11
22
|
for (const ext of extensions) {
|
|
@@ -15,6 +26,26 @@ function partitionByService(extensions) {
|
|
|
15
26
|
}
|
|
16
27
|
return map;
|
|
17
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The options a custom-host client (`service: 'custom'`) inherits from the
|
|
31
|
+
* SDK's resolved bundle: identity and cancellation only. Every
|
|
32
|
+
* Heroku-credential-domain option — auth (`token`, `reauthorize`, `twoFactor`),
|
|
33
|
+
* routing (`headers`, `region`, `defaultAccept`), and consumer
|
|
34
|
+
* `hooks`/`onResponseMetadata` — is intentionally dropped so it never reaches a
|
|
35
|
+
* third-party origin (logplex, busl, exec-manager): sending the bearer token or
|
|
36
|
+
* a consumer `Authorization` header cross-origin would leak credentials, and
|
|
37
|
+
* inheriting `reauthorize` would turn a non-Heroku 401 into a spurious
|
|
38
|
+
* `heroku login`.
|
|
39
|
+
*/
|
|
40
|
+
function pickCustomHostOptions(options) {
|
|
41
|
+
return {
|
|
42
|
+
...(options.debug !== undefined && { debug: options.debug }),
|
|
43
|
+
...(options.signal !== undefined && { signal: options.signal }),
|
|
44
|
+
...(options.timeout !== undefined && { timeout: options.timeout }),
|
|
45
|
+
...(options.userAgent !== undefined && { userAgent: options.userAgent }),
|
|
46
|
+
...(options.warn !== undefined && { warn: options.warn }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
18
49
|
export class HerokuSDK {
|
|
19
50
|
#clientOptions;
|
|
20
51
|
#clientOptionsByService;
|
|
@@ -72,6 +103,7 @@ export class HerokuSDK {
|
|
|
72
103
|
const serviceOptions = { ...serviceOverride };
|
|
73
104
|
delete serviceOptions.service;
|
|
74
105
|
return {
|
|
106
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
75
107
|
...this.#clientOptions,
|
|
76
108
|
...serviceOptions,
|
|
77
109
|
};
|
|
@@ -86,6 +118,17 @@ export class HerokuSDK {
|
|
|
86
118
|
enumerable: true,
|
|
87
119
|
get: () => this.#getRawData(),
|
|
88
120
|
},
|
|
121
|
+
makeClient: {
|
|
122
|
+
enumerable: true,
|
|
123
|
+
value: (inheritFrom, overrides = {}) => {
|
|
124
|
+
const resolved = this.#getClientOptions(inheritFrom);
|
|
125
|
+
// A custom host is not the Heroku credential domain: start from
|
|
126
|
+
// identity/cancellation only, never the auth/routing options that
|
|
127
|
+
// would leak across origins or drive a re-login on a foreign 401.
|
|
128
|
+
const base = overrides.service === 'custom' ? pickCustomHostOptions(resolved) : resolved;
|
|
129
|
+
return new HerokuApiClient({ ...base, ...overrides });
|
|
130
|
+
},
|
|
131
|
+
},
|
|
89
132
|
metrics: {
|
|
90
133
|
enumerable: true,
|
|
91
134
|
get: () => this.#getRawMetrics(),
|
|
@@ -122,7 +165,7 @@ export class HerokuSDK {
|
|
|
122
165
|
return this.#rawMetrics;
|
|
123
166
|
}
|
|
124
167
|
#getRawNotifications() {
|
|
125
|
-
this.#rawNotifications ??= createNotificationsClient(this.#
|
|
168
|
+
this.#rawNotifications ??= createNotificationsClient(this.#getClientOptions('notifications'));
|
|
126
169
|
return this.#rawNotifications;
|
|
127
170
|
}
|
|
128
171
|
#getRawPlatform() {
|
|
@@ -109,8 +109,13 @@ export type ExchangeExecCredentialsOptions = {
|
|
|
109
109
|
* `Authorization` header survive). The caller feeds the returned info,
|
|
110
110
|
* plus the private key it holds, to its own SSH transport; the SDK does
|
|
111
111
|
* not open the connection.
|
|
112
|
+
*
|
|
113
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
114
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
115
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
116
|
+
* pre-threading bare construction.
|
|
112
117
|
*/
|
|
113
|
-
export declare function exchangeExecCredentials(appIdentity: string, options: ExchangeExecCredentialsOptions): Promise<ExecCredentials>;
|
|
118
|
+
export declare function exchangeExecCredentials(appIdentity: string, options: ExchangeExecCredentialsOptions, ctx?: Pick<ResourceCtx, 'makeClient'>): Promise<ExecCredentials>;
|
|
114
119
|
/** A running exec reservation, as reported by the exec-manager status endpoint. */
|
|
115
120
|
export type ExecReservation = {
|
|
116
121
|
[key: string]: unknown;
|
|
@@ -130,5 +135,10 @@ export type ExecStatusOptions = {
|
|
|
130
135
|
* custom-host Basic-auth setup as {@link exchangeExecCredentials}. The
|
|
131
136
|
* caller correlates each reservation's `dyno_name` with live dyno state
|
|
132
137
|
* for display.
|
|
138
|
+
*
|
|
139
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
140
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
141
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
142
|
+
* pre-threading bare construction.
|
|
133
143
|
*/
|
|
134
|
-
export declare function execStatus(appIdentity: string, options: ExecStatusOptions): Promise<ExecReservation[]>;
|
|
144
|
+
export declare function execStatus(appIdentity: string, options: ExecStatusOptions, ctx?: Pick<ResourceCtx, 'makeClient'>): Promise<ExecReservation[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NotFoundError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
2
3
|
import { withSignal } from '../../../core/with-signal.js';
|
|
3
4
|
import { wait } from '../../../utils/wait.js';
|
|
4
5
|
import { waitForInfo } from './wait-for-info.js';
|
|
@@ -107,11 +108,16 @@ export async function restartForExec(ctx, appIdentity, dynoIdentity, options = {
|
|
|
107
108
|
* `Authorization` header survive). The caller feeds the returned info,
|
|
108
109
|
* plus the private key it holds, to its own SSH transport; the SDK does
|
|
109
110
|
* not open the connection.
|
|
111
|
+
*
|
|
112
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
113
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
114
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
115
|
+
* pre-threading bare construction.
|
|
110
116
|
*/
|
|
111
|
-
export async function exchangeExecCredentials(appIdentity, options) {
|
|
117
|
+
export async function exchangeExecCredentials(appIdentity, options, ctx = {}) {
|
|
112
118
|
options.signal?.throwIfAborted();
|
|
113
119
|
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
114
|
-
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
120
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions, ctx);
|
|
115
121
|
const path = `${endpoint.apiPath}/${encodeURIComponent(options.dyno)}`;
|
|
116
122
|
const response = await client.put(path, { client_key: options.publicKey }, {
|
|
117
123
|
headers: {
|
|
@@ -130,11 +136,16 @@ export async function exchangeExecCredentials(appIdentity, options) {
|
|
|
130
136
|
* custom-host Basic-auth setup as {@link exchangeExecCredentials}. The
|
|
131
137
|
* caller correlates each reservation's `dyno_name` with live dyno state
|
|
132
138
|
* for display.
|
|
139
|
+
*
|
|
140
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
141
|
+
* resolved client identity and cancellation signal into the custom-host
|
|
142
|
+
* client. Standalone callers omit it, and the client falls back to the
|
|
143
|
+
* pre-threading bare construction.
|
|
133
144
|
*/
|
|
134
|
-
export async function execStatus(appIdentity, options) {
|
|
145
|
+
export async function execStatus(appIdentity, options, ctx = {}) {
|
|
135
146
|
options.signal?.throwIfAborted();
|
|
136
147
|
const endpoint = resolveExecEndpoint(appIdentity, options.apiKey, options.configVars);
|
|
137
|
-
const client = newExecClient(endpoint.baseUrl, options.clientOptions);
|
|
148
|
+
const client = newExecClient(endpoint.baseUrl, options.clientOptions, ctx);
|
|
138
149
|
const response = await client.get(endpoint.apiPath, {
|
|
139
150
|
headers: { ...options.headers, Authorization: endpoint.authorization },
|
|
140
151
|
signal: options.signal,
|
|
@@ -169,10 +180,13 @@ function resolveExecEndpoint(appIdentity, apiKey, configVars) {
|
|
|
169
180
|
baseUrl: `${url.protocol}//${url.host}`,
|
|
170
181
|
};
|
|
171
182
|
}
|
|
172
|
-
function newExecClient(baseUrl, clientOptions) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
function newExecClient(baseUrl, clientOptions, ctx = {}) {
|
|
184
|
+
// exec-manager is a custom host with Basic auth embedded per request. The
|
|
185
|
+
// custom-host client forces `token: ''` and inherits only the SDK's identity
|
|
186
|
+
// (User-Agent) and signal — never `reauthorize`/headers, so a bad-Basic 401
|
|
187
|
+
// can't drive a spurious `heroku login`. The caller's `clientOptions` still
|
|
188
|
+
// apply on top.
|
|
189
|
+
return makeCustomHostClient(ctx, baseUrl, clientOptions);
|
|
176
190
|
}
|
|
177
191
|
function toBasicAuth(username, password) {
|
|
178
192
|
return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
|
|
@@ -35,10 +35,10 @@ export async function restartDynos(ctx, appIdentity, target, options = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
export const dynoExtensions = extendResource('platform', 'dyno', ctx => ({
|
|
37
37
|
enableExec: (appIdentity, options) => (enableExec(ctx, appIdentity, options)),
|
|
38
|
-
exchangeExecCredentials: (appIdentity, options) => (exchangeExecCredentials(appIdentity, options)),
|
|
38
|
+
exchangeExecCredentials: (appIdentity, options) => (exchangeExecCredentials(appIdentity, options, ctx)),
|
|
39
39
|
execPrereqs: (appIdentity, options) => (execPrereqs(ctx, appIdentity, options)),
|
|
40
|
-
execStatus: (appIdentity, options) => (execStatus(appIdentity, options)),
|
|
41
|
-
listExtended: (appIdentity, options) => (listExtended(appIdentity, options)),
|
|
40
|
+
execStatus: (appIdentity, options) => (execStatus(appIdentity, options, ctx)),
|
|
41
|
+
listExtended: (appIdentity, options) => (listExtended(appIdentity, options, ctx)),
|
|
42
42
|
restart: (appIdentity, target, options) => (restartDynos(ctx, appIdentity, target, options)),
|
|
43
43
|
restartForExec: (appIdentity, dynoIdentity, options) => (restartForExec(ctx, appIdentity, dynoIdentity, options)),
|
|
44
44
|
run: (appIdentity, command, options) => (runDyno(ctx, appIdentity, command, options)),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
2
2
|
import type { Dyno } from '@heroku/types/3.sdk';
|
|
3
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
4
|
/**
|
|
4
5
|
* Operational detail the platform attaches to each dyno only when the
|
|
5
6
|
* `?extended=true` query is used with the `version=3.sdk` Accept
|
|
@@ -46,5 +47,10 @@ export type ListExtendedOptions = {
|
|
|
46
47
|
*
|
|
47
48
|
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
48
49
|
* single GET, matching the historical CLI behavior.
|
|
50
|
+
*
|
|
51
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
52
|
+
* resolved client identity and cancellation signal into the bare client.
|
|
53
|
+
* Standalone callers omit it, and the client falls back to the pre-threading
|
|
54
|
+
* bare construction.
|
|
49
55
|
*/
|
|
50
|
-
export declare function listExtended(appIdentity: string, options?: ListExtendedOptions): Promise<DynoExtended[]>;
|
|
56
|
+
export declare function listExtended(appIdentity: string, options?: ListExtendedOptions, ctx?: Pick<ResourceCtx, 'makeClient'>): Promise<DynoExtended[]>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { makeCtxClient } from '../../../core/extend-resource.js';
|
|
2
2
|
const ACCEPT_EXTENDED = 'application/vnd.heroku+json; version=3.sdk';
|
|
3
3
|
/**
|
|
4
4
|
* List an app's dynos with the extended operational detail block.
|
|
@@ -12,10 +12,15 @@ const ACCEPT_EXTENDED = 'application/vnd.heroku+json; version=3.sdk';
|
|
|
12
12
|
*
|
|
13
13
|
* Unlike the routes client, this does not auto-paginate — it issues a
|
|
14
14
|
* single GET, matching the historical CLI behavior.
|
|
15
|
+
*
|
|
16
|
+
* @param ctx - Internal: supplied by the SDK's dyno extension to thread the
|
|
17
|
+
* resolved client identity and cancellation signal into the bare client.
|
|
18
|
+
* Standalone callers omit it, and the client falls back to the pre-threading
|
|
19
|
+
* bare construction.
|
|
15
20
|
*/
|
|
16
|
-
export async function listExtended(appIdentity, options = {}) {
|
|
21
|
+
export async function listExtended(appIdentity, options = {}, ctx = {}) {
|
|
17
22
|
options.signal?.throwIfAborted();
|
|
18
|
-
const apiClient =
|
|
23
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
19
24
|
...options.clientOptions,
|
|
20
25
|
service: 'platform',
|
|
21
26
|
});
|
|
@@ -73,4 +73,4 @@ export type RunDynoOptions = {
|
|
|
73
73
|
* open their own interactive session; the SDK does not establish the
|
|
74
74
|
* session, spawn ssh, or manage TTY state.
|
|
75
75
|
*/
|
|
76
|
-
export declare function runDyno(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, command: string, options?: RunDynoOptions): Promise<Dyno>;
|
|
76
|
+
export declare function runDyno(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, appIdentity: string, command: string, options?: RunDynoOptions): Promise<Dyno>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HerokuApiError } from '@heroku/heroku-fetch';
|
|
2
|
+
import { makeCtxClient } from '../../../core/extend-resource.js';
|
|
2
3
|
import { withSignal } from '../../../core/with-signal.js';
|
|
3
4
|
const DEFAULT_RUN_RETRIES = 2;
|
|
4
5
|
const ACCEPT_RUN_INSIDE = 'application/vnd.heroku+json; version=3.sdk';
|
|
@@ -59,7 +60,7 @@ export async function runDyno(ctx, appIdentity, command, options = {}) {
|
|
|
59
60
|
}
|
|
60
61
|
async function postRun(ctx, appIdentity, body, options) {
|
|
61
62
|
if (options.dyno) {
|
|
62
|
-
const apiClient =
|
|
63
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
63
64
|
...options.clientOptions,
|
|
64
65
|
service: 'platform',
|
|
65
66
|
});
|
|
@@ -95,7 +95,7 @@ export type StreamLogsOptions = {
|
|
|
95
95
|
* - non-2xx response from the logplex URL
|
|
96
96
|
* - errors thrown by `logSession.create` (auth, app-not-found, etc.)
|
|
97
97
|
*/
|
|
98
|
-
export declare function streamLogs(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: StreamLogsOptions): AsyncGenerator<string, void, unknown>;
|
|
98
|
+
export declare function streamLogs(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, appIdentity: string, options?: StreamLogsOptions): AsyncGenerator<string, void, unknown>;
|
|
99
99
|
export declare const logSessionExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "logSession", {
|
|
100
100
|
parseHerokuLogLine: typeof parseHerokuLogLine;
|
|
101
101
|
streamLogs: (appIdentity: string, options?: StreamLogsOptions) => AsyncGenerator<string, void, unknown>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
1
|
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { extendResource, makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
4
3
|
import { withSignal } from '../../../core/with-signal.js';
|
|
5
4
|
import { getGeneration } from '../app/index.js';
|
|
6
5
|
import { parseHerokuLogLine } from './parser.js';
|
|
@@ -68,15 +67,12 @@ export async function* streamLogs(ctx, appIdentity, options = {}) {
|
|
|
68
67
|
try {
|
|
69
68
|
// Logplex URLs hit a region-specific stream host (e.g.
|
|
70
69
|
// virginia.sessions.logs.heroku.com). The token is embedded in
|
|
71
|
-
// the URL, so we use a custom-
|
|
72
|
-
//
|
|
73
|
-
// env-var proxy
|
|
70
|
+
// the URL, so we use a custom-host client with no auth and a
|
|
71
|
+
// per-call baseUrl, inheriting only the SDK's identity and
|
|
72
|
+
// cancellation; this also picks up heroku-fetch's env-var proxy
|
|
73
|
+
// support.
|
|
74
74
|
const parsed = new URL(session.logplex_url);
|
|
75
|
-
const logplexClient =
|
|
76
|
-
baseUrl: parsed.origin,
|
|
77
|
-
service: 'custom',
|
|
78
|
-
token: '',
|
|
79
|
-
});
|
|
75
|
+
const logplexClient = makeCustomHostClient(ctx, parsed.origin);
|
|
80
76
|
// Fir's telemetry-proxy sends newline-delimited plain text by
|
|
81
77
|
// default, but concatenates records with zero delimiter bytes —
|
|
82
78
|
// splitting on '\n' yields nothing. Requesting SSE flips the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
1
2
|
import type { Pipeline } from '@heroku/types/3.sdk';
|
|
2
|
-
import { type HerokuApiClientOptions } from '@heroku/heroku-fetch';
|
|
3
3
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
4
4
|
export type ResolvePipelineOptions = {
|
|
5
5
|
clientOptions?: HerokuApiClientOptions;
|
|
@@ -47,7 +47,7 @@ export declare class PipelineAmbiguousError extends Error {
|
|
|
47
47
|
* that want to prompt the user should catch this and inspect
|
|
48
48
|
* `error.matches`.
|
|
49
49
|
*/
|
|
50
|
-
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
50
|
+
export declare function resolvePipeline(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, identity: string, options?: ResolvePipelineOptions): Promise<Pipeline>;
|
|
51
51
|
export declare const pipelineExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "pipeline", {
|
|
52
52
|
resolve: (identity: string, options?: ResolvePipelineOptions) => Promise<Pipeline>;
|
|
53
53
|
}>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { extendResource } from '../../../core/extend-resource.js';
|
|
1
|
+
import { extendResource, makeCtxClient } from '../../../core/extend-resource.js';
|
|
3
2
|
import { withSignal } from '../../../core/with-signal.js';
|
|
4
3
|
export class PipelineNotFoundError extends Error {
|
|
5
4
|
identity;
|
|
@@ -56,7 +55,7 @@ export async function resolvePipeline(ctx, identity, options = {}) {
|
|
|
56
55
|
// Bind the signal so the lookup aborts in flight.
|
|
57
56
|
return withSignal(ctx.platform, options.signal).pipeline.info(identity);
|
|
58
57
|
}
|
|
59
|
-
const apiClient =
|
|
58
|
+
const apiClient = makeCtxClient(ctx, 'platform', {
|
|
60
59
|
...options.clientOptions,
|
|
61
60
|
service: 'platform',
|
|
62
61
|
});
|
|
@@ -15,7 +15,7 @@ export type PromotePipelineResult = {
|
|
|
15
15
|
promotion: PipelinePromotion;
|
|
16
16
|
targets: PipelinePromotionTarget[];
|
|
17
17
|
};
|
|
18
|
-
export declare function promotePipeline(ctx: Pick<ResourceCtx, 'platform'>, body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions): Promise<PromotePipelineResult>;
|
|
18
|
+
export declare function promotePipeline(ctx: Pick<ResourceCtx, 'makeClient' | 'platform'>, body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions): Promise<PromotePipelineResult>;
|
|
19
19
|
export declare const pipelinePromotionExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "pipelinePromotion", {
|
|
20
20
|
promote: (body: PipelinePromotionCreateOpts, options?: PromotePipelineOptions) => Promise<PromotePipelineResult>;
|
|
21
21
|
}>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { HerokuApiClient } from '@heroku/heroku-fetch';
|
|
2
1
|
import createDebug from 'debug';
|
|
3
|
-
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { extendResource, makeCustomHostClient } from '../../../core/extend-resource.js';
|
|
4
3
|
import { withSignal } from '../../../core/with-signal.js';
|
|
5
4
|
import { isAbortError } from '../../../utils/is-abort-error.js';
|
|
6
5
|
import { wait } from '../../../utils/wait.js';
|
|
@@ -20,7 +19,7 @@ export async function promotePipeline(ctx, body, options = {}) {
|
|
|
20
19
|
debug('promote created promotion=%s intervalMs=%d timeoutMs=%s', promotion.id, intervalMs, timeoutMs ?? '<none>');
|
|
21
20
|
const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
|
|
22
21
|
const streamRunner = onReleaseStream
|
|
23
|
-
? createReleaseStreamRunner(platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
22
|
+
? createReleaseStreamRunner(ctx, platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal)
|
|
24
23
|
: undefined;
|
|
25
24
|
let poll = 0;
|
|
26
25
|
while (true) {
|
|
@@ -48,7 +47,7 @@ export async function promotePipeline(ctx, body, options = {}) {
|
|
|
48
47
|
* output stream and hands it to `onReleaseStream`. Subsequent calls are
|
|
49
48
|
* no-ops; the stream is one-shot per promotion.
|
|
50
49
|
*/
|
|
51
|
-
function createReleaseStreamRunner(platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
50
|
+
function createReleaseStreamRunner(ctx, platform, onReleaseStream, releaseStreamMaxAttempts, intervalMs, signal) {
|
|
52
51
|
let handled = false;
|
|
53
52
|
return async (targets) => {
|
|
54
53
|
if (handled || targets.length !== 1)
|
|
@@ -64,24 +63,21 @@ function createReleaseStreamRunner(platform, onReleaseStream, releaseStreamMaxAt
|
|
|
64
63
|
return;
|
|
65
64
|
}
|
|
66
65
|
debug('promote release-stream fetching url=%s maxAttempts=%d', release.output_stream_url, releaseStreamMaxAttempts);
|
|
67
|
-
const stream = await fetchReleaseOutput(release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
66
|
+
const stream = await fetchReleaseOutput(ctx, release.output_stream_url, releaseStreamMaxAttempts, intervalMs, signal);
|
|
68
67
|
await onReleaseStream({ stream, target });
|
|
69
68
|
};
|
|
70
69
|
}
|
|
71
70
|
export const pipelinePromotionExtensions = extendResource('platform', 'pipelinePromotion', ctx => ({
|
|
72
71
|
promote: (body, options) => promotePipeline(ctx, body, options),
|
|
73
72
|
}));
|
|
74
|
-
async function fetchReleaseOutput(url, maxAttempts, intervalMs, signal) {
|
|
73
|
+
async function fetchReleaseOutput(ctx, url, maxAttempts, intervalMs, signal) {
|
|
75
74
|
const parsed = new URL(url);
|
|
76
75
|
const path = `${parsed.pathname}${parsed.search}`;
|
|
77
76
|
// The release output URL points at a third-party stream host (e.g. busl).
|
|
78
|
-
// Use a custom-
|
|
79
|
-
// don't leak across origins
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
service: 'custom',
|
|
83
|
-
token: '',
|
|
84
|
-
});
|
|
77
|
+
// Use a custom-host client so the platform prefixUrl, bearer token, and
|
|
78
|
+
// other auth/routing options don't leak across origins, while still
|
|
79
|
+
// inheriting the SDK's identity (User-Agent) and cancellation signal.
|
|
80
|
+
const buslClient = makeCustomHostClient(ctx, parsed.origin);
|
|
85
81
|
let attempt = 0;
|
|
86
82
|
while (true) {
|
|
87
83
|
signal?.throwIfAborted();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "0.12.0";
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -123,19 +123,24 @@
|
|
|
123
123
|
},
|
|
124
124
|
"scripts": {
|
|
125
125
|
"clean": "rimraf dist",
|
|
126
|
+
"generate:version": "node scripts/write-version.mjs",
|
|
127
|
+
"prebuild": "npm run generate:version",
|
|
126
128
|
"build": "npm run clean && tsc",
|
|
129
|
+
"prelint": "npm run generate:version",
|
|
127
130
|
"lint": "eslint .",
|
|
128
131
|
"lint:fix": "eslint . --fix",
|
|
132
|
+
"pretest": "npm run generate:version",
|
|
129
133
|
"test": "vitest run",
|
|
130
134
|
"test:watch": "vitest",
|
|
131
135
|
"test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=lcovonly",
|
|
136
|
+
"pretest:types": "npm run generate:version",
|
|
132
137
|
"test:types": "tsc --noEmit -p tsconfig.type-tests.json",
|
|
133
138
|
"prepare": "npm run build",
|
|
134
139
|
"example": "tsx"
|
|
135
140
|
},
|
|
136
141
|
"license": "Apache-2.0",
|
|
137
142
|
"engines": {
|
|
138
|
-
"node": ">=
|
|
143
|
+
"node": ">=22"
|
|
139
144
|
},
|
|
140
145
|
"files": [
|
|
141
146
|
"dist"
|
|
@@ -145,16 +150,16 @@
|
|
|
145
150
|
"@types/debug": "^4.1.12",
|
|
146
151
|
"@types/node": "^26.5.1",
|
|
147
152
|
"@vitest/coverage-v8": "^4.1.8",
|
|
153
|
+
"@vitest/eslint-plugin": "^1.6.0",
|
|
148
154
|
"eslint": "^9.39.4",
|
|
149
155
|
"eslint-config-oclif": "^6.0.167",
|
|
150
|
-
"@vitest/eslint-plugin": "^1.6.0",
|
|
151
156
|
"rimraf": "^6.1.3",
|
|
152
157
|
"tsx": "^4.22.4",
|
|
153
158
|
"typescript": "^6.0.2",
|
|
154
159
|
"vitest": "^4.1.4"
|
|
155
160
|
},
|
|
156
161
|
"dependencies": {
|
|
157
|
-
"@heroku/heroku-fetch": "^0.2.
|
|
162
|
+
"@heroku/heroku-fetch": "^0.2.3",
|
|
158
163
|
"@heroku/types": "^5.0.0",
|
|
159
164
|
"debug": "^4.4.0"
|
|
160
165
|
}
|