@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.
Files changed (41) hide show
  1. package/dist/core/create-client.d.ts +7 -2
  2. package/dist/core/create-client.js +5 -2
  3. package/dist/core/extend-resource.d.ts +52 -2
  4. package/dist/core/extend-resource.js +27 -0
  5. package/dist/core/heroku-sdk.js +44 -1
  6. package/dist/resources/data/database-credential/create.d.ts +17 -0
  7. package/dist/resources/data/database-credential/create.js +28 -0
  8. package/dist/resources/data/database-credential/destroy.d.ts +17 -0
  9. package/dist/resources/data/database-credential/destroy.js +55 -0
  10. package/dist/resources/data/database-credential/get-url.d.ts +25 -0
  11. package/dist/resources/data/database-credential/get-url.js +68 -0
  12. package/dist/resources/data/database-credential/index.d.ts +24 -0
  13. package/dist/resources/data/database-credential/index.js +24 -0
  14. package/dist/resources/data/database-credential/list.d.ts +32 -0
  15. package/dist/resources/data/database-credential/list.js +44 -0
  16. package/dist/resources/data/database-credential/rotate.d.ts +56 -0
  17. package/dist/resources/data/database-credential/rotate.js +71 -0
  18. package/dist/resources/data/internal/parse-attachment-factors.d.ts +25 -0
  19. package/dist/resources/data/internal/parse-attachment-factors.js +37 -0
  20. package/dist/resources/data/internal/postgres-utils.d.ts +6 -0
  21. package/dist/resources/data/internal/postgres-utils.js +6 -0
  22. package/dist/resources/data/postgres-database.d.ts +3 -2
  23. package/dist/resources/data/postgres-database.js +2 -1
  24. package/dist/resources/extensions/data.d.ts +1 -0
  25. package/dist/resources/extensions/data.js +1 -0
  26. package/dist/resources/platform/dyno/exec.d.ts +12 -2
  27. package/dist/resources/platform/dyno/exec.js +23 -9
  28. package/dist/resources/platform/dyno/index.js +3 -3
  29. package/dist/resources/platform/dyno/list-extended.d.ts +7 -1
  30. package/dist/resources/platform/dyno/list-extended.js +8 -3
  31. package/dist/resources/platform/dyno/run.d.ts +1 -1
  32. package/dist/resources/platform/dyno/run.js +3 -2
  33. package/dist/resources/platform/log-session/index.d.ts +1 -1
  34. package/dist/resources/platform/log-session/index.js +6 -10
  35. package/dist/resources/platform/pipeline/index.d.ts +2 -2
  36. package/dist/resources/platform/pipeline/index.js +2 -3
  37. package/dist/resources/platform/pipeline/promotion.d.ts +1 -1
  38. package/dist/resources/platform/pipeline/promotion.js +9 -13
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +2 -0
  41. 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, but only if
15
- // the caller actually set them. Otherwise the previous value persists.
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 = BaseResourceCtx & {
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
  }
@@ -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.#clientOptions);
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 {};