@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
|
@@ -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() {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PostgresCredentialCreateOpts, PostgresCredentialCreateResult, PostgresDatabaseCreateCredentialsResult } from '@heroku/types/data';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { ResolvedAddOn } from '../../platform/add-on/types.js';
|
|
4
|
+
import type { DatabaseCredentialOptions } from './index.js';
|
|
5
|
+
export type CreateDatabaseCredentialResult = {
|
|
6
|
+
addon: ResolvedAddOn;
|
|
7
|
+
credential: PostgresCredentialCreateResult | PostgresDatabaseCreateCredentialsResult;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a credential on a Postgres database, dispatching to the
|
|
11
|
+
* Advanced-tier or classic-tier endpoint based on the resolved add-on's plan.
|
|
12
|
+
*
|
|
13
|
+
* Essential and legacy Essential-tier databases don't support custom credentials.
|
|
14
|
+
*
|
|
15
|
+
* Returns the resolved addon alongside the created credential
|
|
16
|
+
*/
|
|
17
|
+
export declare function create(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, body: PostgresCredentialCreateOpts, options?: DatabaseCredentialOptions): Promise<CreateDatabaseCredentialResult>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
|
|
3
|
+
import { getPostgresServiceName, isAdvancedDatabase, isEssentialTier } from '../internal/postgres-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a credential on a Postgres database, dispatching to the
|
|
6
|
+
* Advanced-tier or classic-tier endpoint based on the resolved add-on's plan.
|
|
7
|
+
*
|
|
8
|
+
* Essential and legacy Essential-tier databases don't support custom credentials.
|
|
9
|
+
*
|
|
10
|
+
* Returns the resolved addon alongside the created credential
|
|
11
|
+
*/
|
|
12
|
+
export async function create(ctx, addonIdentity, body, options = {}) {
|
|
13
|
+
const { appIdentity, signal } = options;
|
|
14
|
+
signal?.throwIfAborted();
|
|
15
|
+
// Bind the signal so the resolve and create requests abort in flight.
|
|
16
|
+
const platform = withSignal(ctx.platform, signal);
|
|
17
|
+
const addon = await resolveAddonInternal(platform, addonIdentity, {
|
|
18
|
+
addonService: getPostgresServiceName(), appIdentity,
|
|
19
|
+
});
|
|
20
|
+
if (isEssentialTier(addon)) {
|
|
21
|
+
throw new Error('You can\'t create custom credentials on Essential-tier databases.');
|
|
22
|
+
}
|
|
23
|
+
const data = withSignal(ctx.data, signal);
|
|
24
|
+
const credential = await (isAdvancedDatabase(addon)
|
|
25
|
+
? data.postgresCredential.create(addon.id, body)
|
|
26
|
+
: data.postgresDatabase.createCredentials(addon.id, body));
|
|
27
|
+
return { addon, credential };
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PostgresDatabaseDeleteCredentialResult } from '@heroku/types/data';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { ResolvedAddOn } from '../../platform/add-on/types.js';
|
|
4
|
+
import type { DatabaseCredentialOptions } from './index.js';
|
|
5
|
+
export type DestroyDatabaseCredentialResult = {
|
|
6
|
+
addon: ResolvedAddOn;
|
|
7
|
+
result: PostgresDatabaseDeleteCredentialResult | void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Destroys a credential on a Postgres database, dispatching to the
|
|
11
|
+
* Advanced-tier or classic-tier endpoint based on the resolved add-on's plan.
|
|
12
|
+
*
|
|
13
|
+
* The owner credential (Advanced tier) and the default credential (classic/Essential
|
|
14
|
+
* tier) can't be destroyed, custom credentials can't be destroyed on Essential-tier
|
|
15
|
+
* databases, and a credential still attached to any app must be detached first.
|
|
16
|
+
*/
|
|
17
|
+
export declare function destroy(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, credName: string, options?: DatabaseCredentialOptions): Promise<DestroyDatabaseCredentialResult>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
|
|
3
|
+
import { parseAttachmentFactors } from '../internal/parse-attachment-factors.js';
|
|
4
|
+
import { getPostgresServiceName, isAdvancedDatabase, isEssentialDatabase, isLegacyEssentialDatabase, } from '../internal/postgres-utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Destroys a credential on a Postgres database, dispatching to the
|
|
7
|
+
* Advanced-tier or classic-tier endpoint based on the resolved add-on's plan.
|
|
8
|
+
*
|
|
9
|
+
* The owner credential (Advanced tier) and the default credential (classic/Essential
|
|
10
|
+
* tier) can't be destroyed, custom credentials can't be destroyed on Essential-tier
|
|
11
|
+
* databases, and a credential still attached to any app must be detached first.
|
|
12
|
+
*/
|
|
13
|
+
export async function destroy(ctx, addonIdentity, credName, options = {}) {
|
|
14
|
+
const { appIdentity, signal } = options;
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
// Bind the signal so the resolve and destroy requests abort in flight.
|
|
17
|
+
const platform = withSignal(ctx.platform, signal);
|
|
18
|
+
const addon = await resolveAddonInternal(platform, addonIdentity, {
|
|
19
|
+
addonService: getPostgresServiceName(), appIdentity,
|
|
20
|
+
});
|
|
21
|
+
const data = withSignal(ctx.data, signal);
|
|
22
|
+
const isEssentialTier = isEssentialDatabase(addon);
|
|
23
|
+
const isLegacyEssentialTier = isLegacyEssentialDatabase(addon);
|
|
24
|
+
const isAdvancedTier = isAdvancedDatabase(addon);
|
|
25
|
+
let credAttachments = [];
|
|
26
|
+
if (isAdvancedTier) {
|
|
27
|
+
const [{ items: availableCreds }, attachments] = await Promise.all([
|
|
28
|
+
data.postgresCredential.list(addon.id),
|
|
29
|
+
platform.addOnAttachment.listByAddOn(addon.id),
|
|
30
|
+
]);
|
|
31
|
+
const ownerCred = availableCreds.find(cred => cred.type === 'owner');
|
|
32
|
+
if (ownerCred?.name === credName) {
|
|
33
|
+
throw new Error('You can\'t destroy the owner credential.');
|
|
34
|
+
}
|
|
35
|
+
credAttachments = attachments.filter(a => parseAttachmentFactors(a.namespace).role === credName);
|
|
36
|
+
}
|
|
37
|
+
else if (isEssentialTier) {
|
|
38
|
+
throw new Error('You can\'t destroy custom credentials on Essential-tier databases.');
|
|
39
|
+
}
|
|
40
|
+
else if (isLegacyEssentialTier || credName === 'default') {
|
|
41
|
+
throw new Error('You can\'t destroy the default credential.');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const attachments = await platform.addOnAttachment.listByAddOn(addon.id);
|
|
45
|
+
credAttachments = attachments.filter(a => a.namespace === `credential:${credName}`);
|
|
46
|
+
}
|
|
47
|
+
const credAttachmentApps = [...new Set(credAttachments.map(a => a.app.name))];
|
|
48
|
+
if (credAttachmentApps.length > 0) {
|
|
49
|
+
throw new Error(`You must detach the credential ${credName} from the app${credAttachmentApps.length > 1 ? 's' : ''} ${credAttachmentApps.join(', ')} before destroying it.`);
|
|
50
|
+
}
|
|
51
|
+
const result = await (isAdvancedTier
|
|
52
|
+
? data.postgresCredential.delete(addon.id, credName)
|
|
53
|
+
: data.postgresDatabase.deleteCredential(addon.id, credName));
|
|
54
|
+
return { addon, result };
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
+
import type { ResolvedAddOn } from '../../platform/add-on/types.js';
|
|
3
|
+
import type { DatabaseCredentialOptions } from './index.js';
|
|
4
|
+
export type CredentialConnectionInfo = {
|
|
5
|
+
credName: string;
|
|
6
|
+
database: string;
|
|
7
|
+
host: string;
|
|
8
|
+
password: string;
|
|
9
|
+
port: number;
|
|
10
|
+
url: string;
|
|
11
|
+
user: string;
|
|
12
|
+
};
|
|
13
|
+
export type GetDatabaseCredentialUrlResult = {
|
|
14
|
+
addon: ResolvedAddOn;
|
|
15
|
+
credentialConnectionInfo: CredentialConnectionInfo;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Looks up the active connection info for a Postgres database credential,
|
|
19
|
+
* dispatching to the Advanced-tier or classic-tier endpoint based on the
|
|
20
|
+
* resolved add-on's plan, and returns a ready-to-use connection URL.
|
|
21
|
+
*
|
|
22
|
+
* `credName` defaults to the owner credential on the Advanced tier, or the
|
|
23
|
+
* `default` credential on the classic tier.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUrl(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, credName?: string, options?: DatabaseCredentialOptions): Promise<GetDatabaseCredentialUrlResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
|
|
3
|
+
import { getPostgresServiceName, isAdvancedCredential, isAdvancedDatabase, isEssentialTier, } from '../internal/postgres-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Looks up the active connection info for a Postgres database credential,
|
|
6
|
+
* dispatching to the Advanced-tier or classic-tier endpoint based on the
|
|
7
|
+
* resolved add-on's plan, and returns a ready-to-use connection URL.
|
|
8
|
+
*
|
|
9
|
+
* `credName` defaults to the owner credential on the Advanced tier, or the
|
|
10
|
+
* `default` credential on the classic tier.
|
|
11
|
+
*/
|
|
12
|
+
export async function getUrl(ctx, addonIdentity, credName, options = {}) {
|
|
13
|
+
options.signal?.throwIfAborted();
|
|
14
|
+
// Bind the signal so the resolve and lookup requests abort in flight.
|
|
15
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
16
|
+
const addon = await resolveAddonInternal(platform, addonIdentity, {
|
|
17
|
+
addonService: getPostgresServiceName(), appIdentity: options.appIdentity,
|
|
18
|
+
});
|
|
19
|
+
const data = withSignal(ctx.data, options.signal);
|
|
20
|
+
if (isEssentialTier(addon) && credName && credName !== 'default') {
|
|
21
|
+
throw new Error('Essential-tier databases don\'t support named credentials.');
|
|
22
|
+
}
|
|
23
|
+
let name;
|
|
24
|
+
let info;
|
|
25
|
+
if (isAdvancedDatabase(addon)) {
|
|
26
|
+
name = credName;
|
|
27
|
+
if (!name) {
|
|
28
|
+
const { items } = await data.postgresCredential.list(addon.id);
|
|
29
|
+
name = items.find(item => item.type === 'owner')?.name;
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new Error(`There are no active credentials on the database ${addon.name}.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
info = await data.postgresCredential.info(addon.id, name);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
name = credName || 'default';
|
|
38
|
+
info = await data.postgresDatabase.infoCredential(addon.id, name);
|
|
39
|
+
}
|
|
40
|
+
const activeCred = isAdvancedCredential(info)
|
|
41
|
+
? info.roles.find(r => r.state === 'active')
|
|
42
|
+
: info.credentials.find(c => c.state === 'active');
|
|
43
|
+
if (!activeCred) {
|
|
44
|
+
throw new Error(`The credential ${name} isn't active on the database ${addon.name}.`);
|
|
45
|
+
}
|
|
46
|
+
const credInfo = {
|
|
47
|
+
credName: name,
|
|
48
|
+
database: info.database,
|
|
49
|
+
host: info.host,
|
|
50
|
+
password: activeCred.password,
|
|
51
|
+
port: info.port,
|
|
52
|
+
user: activeCred.user,
|
|
53
|
+
};
|
|
54
|
+
const url = new URL(`postgres://${credInfo.host}/${credInfo.database}`);
|
|
55
|
+
url.port = credInfo.port.toString();
|
|
56
|
+
if (credInfo.user && credInfo.password) {
|
|
57
|
+
url.username = credInfo.user;
|
|
58
|
+
url.password = credInfo.password;
|
|
59
|
+
}
|
|
60
|
+
const credentialConnectionInfo = {
|
|
61
|
+
...credInfo,
|
|
62
|
+
url: url.toString(),
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
addon,
|
|
66
|
+
credentialConnectionInfo,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PostgresCredentialCreateOpts } from '@heroku/types/data';
|
|
2
|
+
import type { RotateAllDatabaseCredentialsResult, RotateDatabaseCredentialOptions, RotateDatabaseCredentialOpts, RotateDatabaseCredentialResult } from './rotate.js';
|
|
3
|
+
export type DatabaseCredentialOptions = {
|
|
4
|
+
appIdentity?: string;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
};
|
|
7
|
+
export { create, type CreateDatabaseCredentialResult } from './create.js';
|
|
8
|
+
export { destroy, type DestroyDatabaseCredentialResult } from './destroy.js';
|
|
9
|
+
export { type GetDatabaseCredentialUrlResult, getUrl, } from './get-url.js';
|
|
10
|
+
export { NotAdvancedDatabaseError } from './list.js';
|
|
11
|
+
export { list, type ListDatabaseCredentialsResult } from './list.js';
|
|
12
|
+
export { rotate, type RotateAllDatabaseCredentialsResult, type RotateDatabaseCredentialOptions, type RotateDatabaseCredentialOpts, type RotateDatabaseCredentialPreview, type RotateDatabaseCredentialResult, } from './rotate.js';
|
|
13
|
+
export declare const databaseCredentialExtensions: import("../../../core/extend-resource.js").ResourceExtension<"data", "databaseCredential", {
|
|
14
|
+
create: (addonIdentity: string, body: PostgresCredentialCreateOpts, options?: DatabaseCredentialOptions) => Promise<import("./create.js").CreateDatabaseCredentialResult>;
|
|
15
|
+
destroy: (addonIdentity: string, credName: string, options?: DatabaseCredentialOptions) => Promise<import("./destroy.js").DestroyDatabaseCredentialResult>;
|
|
16
|
+
getUrl: (addonIdentity: string, credName?: string, options?: DatabaseCredentialOptions) => Promise<import("./get-url.js").GetDatabaseCredentialUrlResult>;
|
|
17
|
+
list: (addonIdentity: string, options?: DatabaseCredentialOptions) => Promise<import("./list.js").ListDatabaseCredentialsResult>;
|
|
18
|
+
rotate: {
|
|
19
|
+
(addonIdentity: string, body: RotateDatabaseCredentialOpts & {
|
|
20
|
+
all: true;
|
|
21
|
+
}, options?: RotateDatabaseCredentialOptions): Promise<RotateAllDatabaseCredentialsResult>;
|
|
22
|
+
(addonIdentity: string, body?: RotateDatabaseCredentialOpts, options?: RotateDatabaseCredentialOptions): Promise<RotateDatabaseCredentialResult>;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { create } from './create.js';
|
|
3
|
+
import { destroy } from './destroy.js';
|
|
4
|
+
import { getUrl } from './get-url.js';
|
|
5
|
+
import { list } from './list.js';
|
|
6
|
+
import { rotate } from './rotate.js';
|
|
7
|
+
export { create } from './create.js';
|
|
8
|
+
export { destroy } from './destroy.js';
|
|
9
|
+
export { getUrl, } from './get-url.js';
|
|
10
|
+
export { NotAdvancedDatabaseError } from './list.js';
|
|
11
|
+
export { list } from './list.js';
|
|
12
|
+
export { rotate, } from './rotate.js';
|
|
13
|
+
export const databaseCredentialExtensions = extendResource('data', 'databaseCredential', ctx => {
|
|
14
|
+
function rotateCredential(addonIdentity, body, options) {
|
|
15
|
+
return rotate(ctx, addonIdentity, body, options);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
create: (addonIdentity, body, options) => create(ctx, addonIdentity, body, options),
|
|
19
|
+
destroy: (addonIdentity, credName, options) => destroy(ctx, addonIdentity, credName, options),
|
|
20
|
+
getUrl: (addonIdentity, credName, options) => getUrl(ctx, addonIdentity, credName, options),
|
|
21
|
+
list: (addonIdentity, options) => list(ctx, addonIdentity, options),
|
|
22
|
+
rotate: rotateCredential,
|
|
23
|
+
};
|
|
24
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PostgresCredentialListResult } from '@heroku/types/data';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { ResolvedAddOn } from '../../platform/add-on/types.js';
|
|
4
|
+
import { DatabaseCredentialOptions } from './index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by `list` when the resolved add-on is not an Advanced-tier
|
|
7
|
+
* database. Carries the resolved addon so callers that need its identity
|
|
8
|
+
* (e.g. to point the user at the classic-tier equivalent) don't need a
|
|
9
|
+
* second resolve call.
|
|
10
|
+
*/
|
|
11
|
+
export declare class NotAdvancedDatabaseError extends Error {
|
|
12
|
+
readonly addon: ResolvedAddOn;
|
|
13
|
+
readonly id = "not_advanced_database";
|
|
14
|
+
constructor(addon: ResolvedAddOn);
|
|
15
|
+
get body(): {
|
|
16
|
+
id: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type ListDatabaseCredentialsResult = {
|
|
21
|
+
addon: ResolvedAddOn;
|
|
22
|
+
credentials: PostgresCredentialListResult;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Lists credentials on an Advanced-tier Postgres database. Classic-tier
|
|
26
|
+
* databases don't support this endpoint; use `postgresDatabase.listCredentials`
|
|
27
|
+
* for those instead.
|
|
28
|
+
*
|
|
29
|
+
* Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
|
|
30
|
+
* `addonIdentity` doesn't resolve to an Advanced-tier database.
|
|
31
|
+
*/
|
|
32
|
+
export declare function list(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, options?: DatabaseCredentialOptions): Promise<ListDatabaseCredentialsResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { resolveAddonInternal } from '../../platform/add-on/resolve.js';
|
|
3
|
+
import { getPostgresServiceName, isAdvancedDatabase } from '../internal/postgres-utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by `list` when the resolved add-on is not an Advanced-tier
|
|
6
|
+
* database. Carries the resolved addon so callers that need its identity
|
|
7
|
+
* (e.g. to point the user at the classic-tier equivalent) don't need a
|
|
8
|
+
* second resolve call.
|
|
9
|
+
*/
|
|
10
|
+
export class NotAdvancedDatabaseError extends Error {
|
|
11
|
+
addon;
|
|
12
|
+
id = 'not_advanced_database';
|
|
13
|
+
constructor(addon) {
|
|
14
|
+
super(`You can only list credentials on Advanced-tier databases. ${addon.name} isn't an Advanced-tier database.`);
|
|
15
|
+
this.addon = addon;
|
|
16
|
+
this.name = 'NotAdvancedDatabaseError';
|
|
17
|
+
}
|
|
18
|
+
get body() {
|
|
19
|
+
return { id: this.id, message: this.message };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lists credentials on an Advanced-tier Postgres database. Classic-tier
|
|
24
|
+
* databases don't support this endpoint; use `postgresDatabase.listCredentials`
|
|
25
|
+
* for those instead.
|
|
26
|
+
*
|
|
27
|
+
* Throws `NotAdvancedDatabaseError` (carrying the resolved addon) if
|
|
28
|
+
* `addonIdentity` doesn't resolve to an Advanced-tier database.
|
|
29
|
+
*/
|
|
30
|
+
export async function list(ctx, addonIdentity, options = {}) {
|
|
31
|
+
const { appIdentity, signal } = options;
|
|
32
|
+
signal?.throwIfAborted();
|
|
33
|
+
// Bind the signal so the resolve and list requests abort in flight.
|
|
34
|
+
const platform = withSignal(ctx.platform, signal);
|
|
35
|
+
const addon = await resolveAddonInternal(platform, addonIdentity, {
|
|
36
|
+
addonService: getPostgresServiceName(), appIdentity,
|
|
37
|
+
});
|
|
38
|
+
if (!isAdvancedDatabase(addon)) {
|
|
39
|
+
throw new NotAdvancedDatabaseError(addon);
|
|
40
|
+
}
|
|
41
|
+
const data = withSignal(ctx.data, signal);
|
|
42
|
+
const credentials = await data.postgresCredential.list(addon.id);
|
|
43
|
+
return { addon, credentials };
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PostgresDatabaseInfoCredentialResult as NonAdvancedCredentialInfo, PostgresCredentialListResult, PostgresDatabaseRotateCredentialResult, PostgresDatabaseRotateCredentialsResult, PostgresRotateCredentialsResult } from '@heroku/types/data';
|
|
2
|
+
import { AddOnAttachment } from '@heroku/types/3.sdk';
|
|
3
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
4
|
+
import type { ResolvedAddOn } from '../../platform/add-on/types.js';
|
|
5
|
+
import type { DatabaseCredentialOptions } from './index.js';
|
|
6
|
+
type AdvancedCredentialInfo = PostgresCredentialListResult['items'][number];
|
|
7
|
+
export type RotateDatabaseCredentialOptions = DatabaseCredentialOptions & {
|
|
8
|
+
/**
|
|
9
|
+
* Fires before the rotate request, allowing the caller to perform any
|
|
10
|
+
* necessary pre-rotation checks or UI rendering.
|
|
11
|
+
*/
|
|
12
|
+
beforeRotate?: (preview: RotateDatabaseCredentialPreview) => Promise<void> | void;
|
|
13
|
+
};
|
|
14
|
+
export type RotateDatabaseCredentialOpts = {
|
|
15
|
+
/** Rotate every credential on the database instead of a single one. */
|
|
16
|
+
all?: boolean;
|
|
17
|
+
/** Name of the credential to rotate. Defaults to owner or default credential. */
|
|
18
|
+
credName?: string;
|
|
19
|
+
/** Immediately kill connections using the credential(s) being rotated. */
|
|
20
|
+
forced?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Resolved state of a rotation, passed to `beforeRotate` before the request
|
|
24
|
+
* is sent: the add-on, the credential that will rotate, the owner and default
|
|
25
|
+
* credentials, whether the database is Advanced or Essential tier, and the
|
|
26
|
+
* add-on's attachments.
|
|
27
|
+
*/
|
|
28
|
+
export type RotateDatabaseCredentialPreview = {
|
|
29
|
+
addon: ResolvedAddOn;
|
|
30
|
+
attachments: AddOnAttachment[];
|
|
31
|
+
credToRotate?: AdvancedCredentialInfo | NonAdvancedCredentialInfo;
|
|
32
|
+
defaultCred?: NonAdvancedCredentialInfo;
|
|
33
|
+
isAdvancedTier: boolean;
|
|
34
|
+
isEssentialTier: boolean;
|
|
35
|
+
ownerCred?: AdvancedCredentialInfo;
|
|
36
|
+
};
|
|
37
|
+
export type RotateAllDatabaseCredentialsResult = {
|
|
38
|
+
addon: ResolvedAddOn;
|
|
39
|
+
credentials: PostgresDatabaseRotateCredentialsResult | PostgresRotateCredentialsResult;
|
|
40
|
+
};
|
|
41
|
+
export type RotateDatabaseCredentialResult = {
|
|
42
|
+
addon: ResolvedAddOn;
|
|
43
|
+
credential: PostgresDatabaseRotateCredentialResult | void;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Rotates one or all credentials on a Postgres database, dispatching to the
|
|
47
|
+
* Advanced-tier or classic-tier endpoint based on the resolved add-on's plan.
|
|
48
|
+
*
|
|
49
|
+
* Essential and legacy Essential-tier databases don't support named-credential
|
|
50
|
+
* rotation.
|
|
51
|
+
*/
|
|
52
|
+
export declare function rotate(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, body: RotateDatabaseCredentialOpts & {
|
|
53
|
+
all: true;
|
|
54
|
+
}, options?: RotateDatabaseCredentialOptions): Promise<RotateAllDatabaseCredentialsResult>;
|
|
55
|
+
export declare function rotate(ctx: Pick<ResourceCtx, 'data' | 'platform'>, addonIdentity: string, body?: RotateDatabaseCredentialOpts, options?: RotateDatabaseCredentialOptions): Promise<RotateDatabaseCredentialResult>;
|
|
56
|
+
export {};
|