@heroku/sdk 0.11.2 → 0.12.1
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/data/database-credential/create.d.ts +17 -0
- package/dist/resources/data/database-credential/create.js +28 -0
- package/dist/resources/data/database-credential/destroy.d.ts +17 -0
- package/dist/resources/data/database-credential/destroy.js +55 -0
- package/dist/resources/data/database-credential/get-url.d.ts +25 -0
- package/dist/resources/data/database-credential/get-url.js +68 -0
- package/dist/resources/data/database-credential/index.d.ts +24 -0
- package/dist/resources/data/database-credential/index.js +24 -0
- package/dist/resources/data/database-credential/list.d.ts +32 -0
- package/dist/resources/data/database-credential/list.js +44 -0
- package/dist/resources/data/database-credential/rotate.d.ts +56 -0
- package/dist/resources/data/database-credential/rotate.js +71 -0
- package/dist/resources/data/internal/parse-attachment-factors.d.ts +25 -0
- package/dist/resources/data/internal/parse-attachment-factors.js +37 -0
- package/dist/resources/data/internal/postgres-utils.d.ts +6 -0
- package/dist/resources/data/internal/postgres-utils.js +6 -0
- package/dist/resources/data/postgres-database.d.ts +3 -2
- package/dist/resources/data/postgres-database.js +2 -1
- package/dist/resources/extensions/data.d.ts +1 -0
- package/dist/resources/extensions/data.js +1 -0
- 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 +15 -6
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
|
|
3
|
+
import { getPostgresServiceName, isAdvancedDatabase, isEssentialDatabase, isLegacyEssentialDatabase, } from '../internal/postgres-utils.js';
|
|
4
|
+
export async function rotate(ctx, addonIdentity, body = {}, options = {}) {
|
|
5
|
+
const { appIdentity, beforeRotate, signal } = options;
|
|
6
|
+
signal?.throwIfAborted();
|
|
7
|
+
// Bind the signal so the resolve and rotate requests abort in flight.
|
|
8
|
+
const platform = withSignal(ctx.platform, signal);
|
|
9
|
+
const addon = await resolveAddonInternal(platform, addonIdentity, {
|
|
10
|
+
addonService: getPostgresServiceName(), appIdentity,
|
|
11
|
+
});
|
|
12
|
+
const { all, credName, forced } = body;
|
|
13
|
+
const isAdvancedTier = isAdvancedDatabase(addon);
|
|
14
|
+
// Check for essential tier only; legacy Essential-tier databases
|
|
15
|
+
// get their own message below and are otherwise treated like
|
|
16
|
+
// any other classic-tier database for credential selection.
|
|
17
|
+
const isEssentialTier = isEssentialDatabase(addon);
|
|
18
|
+
if (!isAdvancedTier && credName && credName !== 'default') {
|
|
19
|
+
if (isLegacyEssentialDatabase(addon)) {
|
|
20
|
+
throw new Error('Legacy Essential-tier databases do not support named credentials.');
|
|
21
|
+
}
|
|
22
|
+
if (isEssentialTier) {
|
|
23
|
+
throw new Error('Essential-tier databases do not support named credentials.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const data = withSignal(ctx.data, signal);
|
|
27
|
+
const attachments = await platform.addOnAttachment.listByAddOn(addon.id);
|
|
28
|
+
let ownerCred;
|
|
29
|
+
let defaultCred;
|
|
30
|
+
let credToRotate;
|
|
31
|
+
if (isAdvancedTier) {
|
|
32
|
+
const { items: availableCreds } = await data.postgresCredential.list(addon.id);
|
|
33
|
+
ownerCred = availableCreds.find(cred => cred.type === 'owner');
|
|
34
|
+
credToRotate = credName
|
|
35
|
+
? availableCreds.find(cred => cred.name === credName)
|
|
36
|
+
: ownerCred;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const availableCreds = await data.postgresDatabase.listCredentials(addon.id);
|
|
40
|
+
defaultCred = isEssentialTier
|
|
41
|
+
? availableCreds[0]
|
|
42
|
+
: availableCreds.find(cred => cred.name === 'default');
|
|
43
|
+
credToRotate = credName && !isEssentialTier
|
|
44
|
+
? availableCreds.find(cred => cred.name === credName)
|
|
45
|
+
: defaultCred;
|
|
46
|
+
}
|
|
47
|
+
await beforeRotate?.({
|
|
48
|
+
addon,
|
|
49
|
+
attachments,
|
|
50
|
+
credToRotate,
|
|
51
|
+
defaultCred,
|
|
52
|
+
isAdvancedTier,
|
|
53
|
+
isEssentialTier,
|
|
54
|
+
ownerCred,
|
|
55
|
+
});
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
if (all) {
|
|
58
|
+
const credentials = await (isAdvancedTier
|
|
59
|
+
? data.postgres.rotateCredentials(addon.id, { forced })
|
|
60
|
+
: data.postgresDatabase.rotateCredentials(addon.id, { forced }));
|
|
61
|
+
return { addon, credentials };
|
|
62
|
+
}
|
|
63
|
+
const finalCredName = isEssentialTier ? 'default' : credToRotate?.name;
|
|
64
|
+
if (!finalCredName) {
|
|
65
|
+
throw new Error(`There are no credentials on the database ${addon.name}.`);
|
|
66
|
+
}
|
|
67
|
+
const credential = await (isAdvancedTier
|
|
68
|
+
? data.postgresCredential.rotate(addon.id, finalCredName, { forced })
|
|
69
|
+
: data.postgresDatabase.rotateCredential(addon.id, finalCredName, { forced }));
|
|
70
|
+
return { addon, credential };
|
|
71
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
declare enum AttachmentFactorKeys {
|
|
2
|
+
POOL = "pool",
|
|
3
|
+
PROXY = "proxy",
|
|
4
|
+
ROLE = "role"
|
|
5
|
+
}
|
|
6
|
+
export type AttachmentFactors = {
|
|
7
|
+
[AttachmentFactorKeys.POOL]?: string;
|
|
8
|
+
[AttachmentFactorKeys.PROXY]?: boolean;
|
|
9
|
+
[AttachmentFactorKeys.ROLE]?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Parses a Heroku add-on attachment `namespace` string into structured factors.
|
|
13
|
+
*
|
|
14
|
+
* The namespace is a list of segments separated by `|`. Each segment must look
|
|
15
|
+
* like `key:value`, using only the **first** `:` as the separator so values may
|
|
16
|
+
* contain additional colons. Whitespace around keys and values is trimmed.
|
|
17
|
+
*
|
|
18
|
+
* Only keys listed in {@link AttachmentFactorKeys} are applied; unknown keys are
|
|
19
|
+
* ignored. For `proxy`, the value is interpreted as a boolean (`true` only when
|
|
20
|
+
* the trimmed string is exactly `"true"`). For `pool` and `role`, the value is
|
|
21
|
+
* kept as a string. If the same key appears more than once, the last occurrence
|
|
22
|
+
* wins.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseAttachmentFactors(namespace?: null | string): AttachmentFactors;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
var AttachmentFactorKeys;
|
|
2
|
+
(function (AttachmentFactorKeys) {
|
|
3
|
+
AttachmentFactorKeys["POOL"] = "pool";
|
|
4
|
+
AttachmentFactorKeys["PROXY"] = "proxy";
|
|
5
|
+
AttachmentFactorKeys["ROLE"] = "role";
|
|
6
|
+
})(AttachmentFactorKeys || (AttachmentFactorKeys = {}));
|
|
7
|
+
/**
|
|
8
|
+
* Parses a Heroku add-on attachment `namespace` string into structured factors.
|
|
9
|
+
*
|
|
10
|
+
* The namespace is a list of segments separated by `|`. Each segment must look
|
|
11
|
+
* like `key:value`, using only the **first** `:` as the separator so values may
|
|
12
|
+
* contain additional colons. Whitespace around keys and values is trimmed.
|
|
13
|
+
*
|
|
14
|
+
* Only keys listed in {@link AttachmentFactorKeys} are applied; unknown keys are
|
|
15
|
+
* ignored. For `proxy`, the value is interpreted as a boolean (`true` only when
|
|
16
|
+
* the trimmed string is exactly `"true"`). For `pool` and `role`, the value is
|
|
17
|
+
* kept as a string. If the same key appears more than once, the last occurrence
|
|
18
|
+
* wins.
|
|
19
|
+
*/
|
|
20
|
+
export function parseAttachmentFactors(namespace) {
|
|
21
|
+
const result = {};
|
|
22
|
+
if (!namespace?.trim()) {
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
for (const factor of namespace.split('|')) {
|
|
26
|
+
const parts = factor.match(/^([^:]+):(.*)$/) ?? [];
|
|
27
|
+
if (parts.length !== 3) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const key = parts[1].trim();
|
|
31
|
+
const value = (key === AttachmentFactorKeys.PROXY ? parts[2].trim() === 'true' : parts[2].trim());
|
|
32
|
+
if (Object.values(AttachmentFactorKeys).includes(key)) {
|
|
33
|
+
Object.assign(result, { [key]: value });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { AddOn } from '@heroku/types/3.sdk';
|
|
2
|
+
import { PostgresCredentialInfoResult as AdvancedCredentialInfo } from '@heroku/types/data';
|
|
2
3
|
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
4
|
import type { AddOnAttachmentWithInclusions } from '../../platform/add-on-attachment/resolve.js';
|
|
5
|
+
import { CredentialInfo as NonAdvancedCredentialInfo } from '../postgres-database.js';
|
|
4
6
|
/**
|
|
5
7
|
* Returns the first legacy database add-on on the app.
|
|
6
8
|
*/
|
|
7
9
|
export declare function findLegacyDatabase(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string): Promise<AddOn>;
|
|
10
|
+
export declare function isAdvancedCredential(credential: AdvancedCredentialInfo | NonAdvancedCredentialInfo): credential is AdvancedCredentialInfo;
|
|
8
11
|
export declare const getPostgresServiceName: () => string;
|
|
9
12
|
/**
|
|
10
13
|
* `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
|
|
@@ -13,3 +16,6 @@ export declare const isPostgresAddonInternal: (addon: AddOn | AddOnAttachmentWit
|
|
|
13
16
|
export declare const isPostgresAddon: (addon: AddOn) => boolean;
|
|
14
17
|
export declare const isAdvancedDatabase: (addon: AddOn) => boolean;
|
|
15
18
|
export declare const isLegacyDatabase: (addon: AddOn) => boolean;
|
|
19
|
+
export declare const isLegacyEssentialDatabase: (addon: AddOn) => boolean;
|
|
20
|
+
export declare const isEssentialDatabase: (addon: AddOn) => boolean;
|
|
21
|
+
export declare const isEssentialTier: (addon: AddOn) => boolean;
|
|
@@ -11,6 +11,9 @@ export async function findLegacyDatabase(ctx, appIdentity) {
|
|
|
11
11
|
throw new Error(`No Heroku Postgres legacy database on ${appIdentity}`);
|
|
12
12
|
return addon;
|
|
13
13
|
}
|
|
14
|
+
export function isAdvancedCredential(credential) {
|
|
15
|
+
return 'roles' in credential;
|
|
16
|
+
}
|
|
14
17
|
export const getPostgresServiceName = () => process.env.HEROKU_POSTGRESQL_ADDON_NAME || process.env.HEROKU_DATA_SERVICE || 'heroku-postgresql';
|
|
15
18
|
/**
|
|
16
19
|
* `AddOnAttachmentWithInclusions` is an internal type. The public `isPostgresAddon` takes `AddOn` only.
|
|
@@ -19,3 +22,6 @@ export const isPostgresAddonInternal = (addon) => addon.plan?.name?.split(':', 2
|
|
|
19
22
|
export const isPostgresAddon = (addon) => isPostgresAddonInternal(addon);
|
|
20
23
|
export const isAdvancedDatabase = (addon) => isPostgresAddonInternal(addon) && /^(advanced|performance)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
|
|
21
24
|
export const isLegacyDatabase = (addon) => isPostgresAddonInternal(addon) && !isAdvancedDatabase(addon);
|
|
25
|
+
export const isLegacyEssentialDatabase = (addon) => isLegacyDatabase(addon) && /^(dev|basic|mini)/.test(addon.plan?.name?.split(':', 2)[1] ?? '');
|
|
26
|
+
export const isEssentialDatabase = (addon) => isLegacyDatabase(addon) && (addon.plan?.name?.split(':', 2)[1] ?? '').startsWith('essential');
|
|
27
|
+
export const isEssentialTier = (addon) => isEssentialDatabase(addon) || isLegacyEssentialDatabase(addon);
|
|
@@ -12,7 +12,7 @@ export type CredentialInfo = {
|
|
|
12
12
|
database: string;
|
|
13
13
|
host: string;
|
|
14
14
|
name: string;
|
|
15
|
-
port:
|
|
15
|
+
port: number;
|
|
16
16
|
state: string;
|
|
17
17
|
uuid: string;
|
|
18
18
|
};
|
|
@@ -20,4 +20,5 @@ export declare function listCredentials(ctx: Pick<ResourceCtx, 'data' | 'platfor
|
|
|
20
20
|
export declare const postgresDatabaseExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "postgresDatabase", {
|
|
21
21
|
listCredentials: (appIdentity: string, addonIdentity?: string, options?: ListCredentialsOptions) => Promise<CredentialInfo[]>;
|
|
22
22
|
}>;
|
|
23
|
-
export {
|
|
23
|
+
export { type AttachmentFactors, parseAttachmentFactors, } from './internal/parse-attachment-factors.js';
|
|
24
|
+
export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
|
|
@@ -11,4 +11,5 @@ export async function listCredentials(ctx, appIdentity, addonIdentity, options =
|
|
|
11
11
|
export const postgresDatabaseExtensions = extendResource('data', 'postgresDatabase', ctx => ({
|
|
12
12
|
listCredentials: (appIdentity, addonIdentity, options) => listCredentials(ctx, appIdentity, addonIdentity, options),
|
|
13
13
|
}));
|
|
14
|
-
export {
|
|
14
|
+
export { parseAttachmentFactors, } from './internal/parse-attachment-factors.js';
|
|
15
|
+
export { findLegacyDatabase, getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialDatabase, isEssentialTier, isLegacyDatabase, isLegacyEssentialDatabase, isPostgresAddon, } from './internal/postgres-utils.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { databaseCredentialExtensions } from '../data/database-credential/index.js';
|
|
1
2
|
export { databaseExtensions } from '../data/database.js';
|
|
2
3
|
export { maintenanceExtensions } from '../data/maintenance.js';
|
|
3
4
|
export { postgresDatabaseExtensions } from '../data/postgres-database.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { databaseCredentialExtensions } from '../data/database-credential/index.js';
|
|
1
2
|
export { databaseExtensions } from '../data/database.js';
|
|
2
3
|
export { maintenanceExtensions } from '../data/maintenance.js';
|
|
3
4
|
export { postgresDatabaseExtensions } from '../data/postgres-database.js';
|
|
@@ -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.1";
|
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.1",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -102,6 +102,10 @@
|
|
|
102
102
|
"types": "./dist/resources/platform/vpn-connection/index.d.ts",
|
|
103
103
|
"default": "./dist/resources/platform/vpn-connection/index.js"
|
|
104
104
|
},
|
|
105
|
+
"./resources/data/database-credential": {
|
|
106
|
+
"types": "./dist/resources/data/database-credential/index.d.ts",
|
|
107
|
+
"default": "./dist/resources/data/database-credential/index.js"
|
|
108
|
+
},
|
|
105
109
|
"./resources/data/redis": {
|
|
106
110
|
"types": "./dist/resources/data/redis/index.d.ts",
|
|
107
111
|
"default": "./dist/resources/data/redis/index.js"
|
|
@@ -123,19 +127,24 @@
|
|
|
123
127
|
},
|
|
124
128
|
"scripts": {
|
|
125
129
|
"clean": "rimraf dist",
|
|
130
|
+
"generate:version": "node scripts/write-version.mjs",
|
|
131
|
+
"prebuild": "npm run generate:version",
|
|
126
132
|
"build": "npm run clean && tsc",
|
|
133
|
+
"prelint": "npm run generate:version",
|
|
127
134
|
"lint": "eslint .",
|
|
128
135
|
"lint:fix": "eslint . --fix",
|
|
136
|
+
"pretest": "npm run generate:version",
|
|
129
137
|
"test": "vitest run",
|
|
130
138
|
"test:watch": "vitest",
|
|
131
139
|
"test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=lcovonly",
|
|
140
|
+
"pretest:types": "npm run generate:version",
|
|
132
141
|
"test:types": "tsc --noEmit -p tsconfig.type-tests.json",
|
|
133
142
|
"prepare": "npm run build",
|
|
134
143
|
"example": "tsx"
|
|
135
144
|
},
|
|
136
145
|
"license": "Apache-2.0",
|
|
137
146
|
"engines": {
|
|
138
|
-
"node": ">=
|
|
147
|
+
"node": ">=22"
|
|
139
148
|
},
|
|
140
149
|
"files": [
|
|
141
150
|
"dist"
|
|
@@ -144,17 +153,17 @@
|
|
|
144
153
|
"@heroku-cli/test-utils": "^1.0.3",
|
|
145
154
|
"@types/debug": "^4.1.12",
|
|
146
155
|
"@types/node": "^26.5.1",
|
|
147
|
-
"@vitest/coverage-v8": "^
|
|
156
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
157
|
+
"@vitest/eslint-plugin": "^1.6.0",
|
|
148
158
|
"eslint": "^9.39.4",
|
|
149
159
|
"eslint-config-oclif": "^6.0.167",
|
|
150
|
-
"@vitest/eslint-plugin": "^1.6.0",
|
|
151
160
|
"rimraf": "^6.1.3",
|
|
152
161
|
"tsx": "^4.22.4",
|
|
153
162
|
"typescript": "^6.0.2",
|
|
154
|
-
"vitest": "^
|
|
163
|
+
"vitest": "^5.0.5"
|
|
155
164
|
},
|
|
156
165
|
"dependencies": {
|
|
157
|
-
"@heroku/heroku-fetch": "^0.2.
|
|
166
|
+
"@heroku/heroku-fetch": "^0.2.3",
|
|
158
167
|
"@heroku/types": "^5.0.0",
|
|
159
168
|
"debug": "^4.4.0"
|
|
160
169
|
}
|