@heroku/sdk 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/core/dispatcher.js +13 -2
- package/dist/resources/dashboard-backend/favorite/add-by-app-name.d.ts +7 -0
- package/dist/resources/dashboard-backend/favorite/add-by-app-name.js +33 -0
- package/dist/resources/dashboard-backend/favorite/errors.d.ts +16 -0
- package/dist/resources/dashboard-backend/favorite/errors.js +32 -0
- package/dist/resources/dashboard-backend/favorite/index.d.ts +8 -0
- package/dist/resources/dashboard-backend/favorite/index.js +10 -0
- package/dist/resources/dashboard-backend/favorite/remove-by-app-name.d.ts +4 -0
- package/dist/resources/dashboard-backend/favorite/remove-by-app-name.js +15 -0
- 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/dashboard-backend.d.ts +1 -0
- package/dist/resources/extensions/dashboard-backend.js +1 -0
- package/dist/resources/extensions/data.d.ts +1 -0
- package/dist/resources/extensions/data.js +1 -0
- package/dist/resources/extensions/platform.d.ts +2 -0
- package/dist/resources/extensions/platform.js +2 -0
- package/dist/resources/platform/app-collaborator/index.d.ts +5 -0
- package/dist/resources/platform/app-collaborator/index.js +6 -0
- package/dist/resources/platform/app-collaborator/remove-self.d.ts +10 -0
- package/dist/resources/platform/app-collaborator/remove-self.js +21 -0
- package/dist/resources/platform/team-app/index.d.ts +5 -0
- package/dist/resources/platform/team-app/index.js +6 -0
- package/dist/resources/platform/team-app/join-self.d.ts +7 -0
- package/dist/resources/platform/team-app/join-self.js +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -133,6 +133,7 @@ const favorites = await dashboardBackend.favorite.list({ type: 'app' })
|
|
|
133
133
|
| `createRepositoriesClient`, `RepositoriesClient` | `@heroku/sdk/repositories` |
|
|
134
134
|
| `createRepositoriesApiClient`, `RepositoriesApiClient` | `@heroku/sdk/repositories-api` |
|
|
135
135
|
| `appExtensions`, `dynoExtensions`, … | `@heroku/sdk/extensions/platform` |
|
|
136
|
+
| `favoriteExtensions`, `addByAppName`, `removeByAppName`, favorite errors and types | `@heroku/sdk/extensions/dashboard-backend` |
|
|
136
137
|
| `databaseExtensions`, … | `@heroku/sdk/extensions/data` |
|
|
137
138
|
|
|
138
139
|
## Development
|
package/dist/core/dispatcher.js
CHANGED
|
@@ -7,11 +7,22 @@ function hasCallerRangeHeader(requestOptions) {
|
|
|
7
7
|
return false;
|
|
8
8
|
return Object.keys(requestOptions.headers).some(k => k.toLowerCase() === 'range');
|
|
9
9
|
}
|
|
10
|
+
function redactEmailPathSegments(path) {
|
|
11
|
+
return path.split('/').map(segment => {
|
|
12
|
+
try {
|
|
13
|
+
return decodeURIComponent(segment).includes('@') ? '[REDACTED]' : segment;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return segment;
|
|
17
|
+
}
|
|
18
|
+
}).join('/');
|
|
19
|
+
}
|
|
10
20
|
export async function dispatch(client, route, args, invocation, requestOptions) {
|
|
11
21
|
const placeholderCount = countPlaceholders(route.path);
|
|
12
22
|
const pathParams = args.slice(0, placeholderCount);
|
|
13
23
|
const remaining = args.slice(placeholderCount);
|
|
14
24
|
const path = interpolatePath(route.path, pathParams);
|
|
25
|
+
const debugPath = redactEmailPathSegments(path);
|
|
15
26
|
let body;
|
|
16
27
|
if (route.hasRequestBody && remaining.length > 0) {
|
|
17
28
|
body = remaining[0];
|
|
@@ -30,7 +41,7 @@ export async function dispatch(client, route, args, invocation, requestOptions)
|
|
|
30
41
|
effectiveOptions = { ...requestOptions, searchParams };
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
|
-
debug('%s %s %s -> %s hasBody=%s', invocation ?? 'dispatch', route.method, route.path,
|
|
44
|
+
debug('%s %s %s -> %s hasBody=%s', invocation ?? 'dispatch', route.method, route.path, debugPath, body !== undefined);
|
|
34
45
|
const response = await callMethod(client, route.method, path, body, effectiveOptions);
|
|
35
46
|
const contentLength = response.headers.get('content-length');
|
|
36
47
|
if (response.status === 204 || contentLength === '0') {
|
|
@@ -87,7 +98,7 @@ function callMethod(client, method, path, body, requestOptions) {
|
|
|
87
98
|
return requestOptions ? client.put(path, body, requestOptions) : client.put(path, body);
|
|
88
99
|
}
|
|
89
100
|
default: {
|
|
90
|
-
debug('unsupported HTTP method=%s path=%s', method, path);
|
|
101
|
+
debug('unsupported HTTP method=%s path=%s', method, redactEmailPathSegments(path));
|
|
91
102
|
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
92
103
|
}
|
|
93
104
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FavoriteCreateResult } from '@heroku/types/dashboard-backend';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
export type FavoriteOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
/** Add an app to the current user's favorites by its exact app name. */
|
|
7
|
+
export declare function addByAppName(ctx: Pick<ResourceCtx, 'dashboardBackend'>, appName: string, options?: FavoriteOptions): Promise<FavoriteCreateResult | void>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { isAbortError } from '../../../utils/is-abort-error.js';
|
|
3
|
+
import { AlreadyFavoriteError, AppNotFoundError, FavoriteCreateError } from './errors.js';
|
|
4
|
+
/** Add an app to the current user's favorites by its exact app name. */
|
|
5
|
+
export async function addByAppName(ctx, appName, options = {}) {
|
|
6
|
+
const { signal } = options;
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
const dashboardBackend = withSignal(ctx.dashboardBackend, signal);
|
|
9
|
+
const favorites = await dashboardBackend.favorite.list({ type: 'app' });
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
if (favorites.some(favorite => favorite.resource_name === appName)) {
|
|
12
|
+
throw new AlreadyFavoriteError(appName);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await dashboardBackend.favorite.create({ resource_id: appName, type: 'app' });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
// Preserve request cancellation, including custom abort reasons, but don't
|
|
19
|
+
// let an unrelated abort race mask the create request's actual failure.
|
|
20
|
+
if (isAbortError(error) || (signal?.aborted && error === signal.reason))
|
|
21
|
+
throw error;
|
|
22
|
+
if (isNotFound(error)) {
|
|
23
|
+
throw new AppNotFoundError(appName, error);
|
|
24
|
+
}
|
|
25
|
+
throw new FavoriteCreateError(appName, error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isNotFound(error) {
|
|
29
|
+
return (typeof error === 'object'
|
|
30
|
+
&& error !== null
|
|
31
|
+
&& 'statusCode' in error
|
|
32
|
+
&& error.statusCode === 404);
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class AlreadyFavoriteError extends Error {
|
|
2
|
+
readonly appName: string;
|
|
3
|
+
constructor(appName: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class NotFavoriteError extends Error {
|
|
6
|
+
readonly appName: string;
|
|
7
|
+
constructor(appName: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class AppNotFoundError extends Error {
|
|
10
|
+
readonly appName: string;
|
|
11
|
+
constructor(appName: string, cause: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare class FavoriteCreateError extends Error {
|
|
14
|
+
readonly appName: string;
|
|
15
|
+
constructor(appName: string, cause: unknown);
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class AlreadyFavoriteError extends Error {
|
|
2
|
+
appName;
|
|
3
|
+
constructor(appName) {
|
|
4
|
+
super(`App ${appName} is already a favorite.`);
|
|
5
|
+
this.appName = appName;
|
|
6
|
+
this.name = 'AlreadyFavoriteError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class NotFavoriteError extends Error {
|
|
10
|
+
appName;
|
|
11
|
+
constructor(appName) {
|
|
12
|
+
super(`App ${appName} is not a favorite.`);
|
|
13
|
+
this.appName = appName;
|
|
14
|
+
this.name = 'NotFavoriteError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class AppNotFoundError extends Error {
|
|
18
|
+
appName;
|
|
19
|
+
constructor(appName, cause) {
|
|
20
|
+
super(`Couldn't find app ${appName}.`, { cause });
|
|
21
|
+
this.appName = appName;
|
|
22
|
+
this.name = 'AppNotFoundError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class FavoriteCreateError extends Error {
|
|
26
|
+
appName;
|
|
27
|
+
constructor(appName, cause) {
|
|
28
|
+
super(`Failed to add app ${appName} as a favorite.`, { cause });
|
|
29
|
+
this.appName = appName;
|
|
30
|
+
this.name = 'FavoriteCreateError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FavoriteOptions } from './add-by-app-name.js';
|
|
2
|
+
export { addByAppName, type FavoriteOptions } from './add-by-app-name.js';
|
|
3
|
+
export { AlreadyFavoriteError, AppNotFoundError, FavoriteCreateError, NotFavoriteError, } from './errors.js';
|
|
4
|
+
export { removeByAppName } from './remove-by-app-name.js';
|
|
5
|
+
export declare const favoriteExtensions: import("../../../core/extend-resource.js").ResourceExtension<"dashboardBackend", "favorite", {
|
|
6
|
+
addByAppName: (appName: string, options?: FavoriteOptions) => Promise<void | import("@heroku/types/dashboard-backend").FavoriteCreateResult>;
|
|
7
|
+
removeByAppName: (appName: string, options?: FavoriteOptions) => Promise<void>;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { addByAppName } from './add-by-app-name.js';
|
|
3
|
+
import { removeByAppName } from './remove-by-app-name.js';
|
|
4
|
+
export { addByAppName } from './add-by-app-name.js';
|
|
5
|
+
export { AlreadyFavoriteError, AppNotFoundError, FavoriteCreateError, NotFavoriteError, } from './errors.js';
|
|
6
|
+
export { removeByAppName } from './remove-by-app-name.js';
|
|
7
|
+
export const favoriteExtensions = extendResource('dashboardBackend', 'favorite', ctx => ({
|
|
8
|
+
addByAppName: (appName, options) => addByAppName(ctx, appName, options),
|
|
9
|
+
removeByAppName: (appName, options) => removeByAppName(ctx, appName, options),
|
|
10
|
+
}));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
2
|
+
import type { FavoriteOptions } from './add-by-app-name.js';
|
|
3
|
+
/** Remove an app from the current user's favorites by its exact app name. */
|
|
4
|
+
export declare function removeByAppName(ctx: Pick<ResourceCtx, 'dashboardBackend'>, appName: string, options?: FavoriteOptions): Promise<void>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { NotFavoriteError } from './errors.js';
|
|
3
|
+
/** Remove an app from the current user's favorites by its exact app name. */
|
|
4
|
+
export async function removeByAppName(ctx, appName, options = {}) {
|
|
5
|
+
const { signal } = options;
|
|
6
|
+
signal?.throwIfAborted();
|
|
7
|
+
const dashboardBackend = withSignal(ctx.dashboardBackend, signal);
|
|
8
|
+
const favorites = await dashboardBackend.favorite.list({ type: 'app' });
|
|
9
|
+
signal?.throwIfAborted();
|
|
10
|
+
const favorite = favorites.find(candidate => candidate.resource_name === appName);
|
|
11
|
+
if (!favorite) {
|
|
12
|
+
throw new NotFavoriteError(appName);
|
|
13
|
+
}
|
|
14
|
+
await dashboardBackend.favorite.delete(favorite.id);
|
|
15
|
+
}
|
|
@@ -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 {};
|
|
@@ -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';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addByAppName, AlreadyFavoriteError, AppNotFoundError, FavoriteCreateError, favoriteExtensions, type FavoriteOptions, NotFavoriteError, removeByAppName, } from '../dashboard-backend/favorite/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { addByAppName, AlreadyFavoriteError, AppNotFoundError, FavoriteCreateError, favoriteExtensions, NotFavoriteError, removeByAppName, } from '../dashboard-backend/favorite/index.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';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { accountExtensions, type AccountWithDelinquency, type GetAccountWithDelinquencyOptions } from '../platform/account.js';
|
|
2
2
|
export { addOnAttachmentExtensions } from '../platform/add-on-attachment/index.js';
|
|
3
3
|
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
4
|
+
export { appCollaboratorExtensions, type BeforeRemove, type RemoveSelfOptions, } from '../platform/app-collaborator/index.js';
|
|
4
5
|
export { appExtensions, type AppInfo, type DiffAppsOptions, type DiffRow, type PipelineCouplingDetail, } from '../platform/app/index.js';
|
|
5
6
|
export { containerExtensions, type DockerReleasesBatchUpdateOpts, NotAContainerAppError, type ReleaseDockerImagesResult, type RemoveProcessTypesOpts, } from '../platform/container/index.js';
|
|
6
7
|
export { type DelinquencyResource, type DelinquencySchedule, formatDelinquencyWarning, } from '../platform/delinquency.js';
|
|
@@ -14,6 +15,7 @@ export { type ResolveRepoNameOptions, reviewAppConfigExtensions } from '../platf
|
|
|
14
15
|
export { privateToShield, shieldToPrivate } from '../platform/shield.js';
|
|
15
16
|
export { createAndAssociate, type CreateAndAssociateOptions, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
|
|
16
17
|
export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, type SpaceOptions, type SpaceWithNat, waitForAllocated, type WaitForAllocatedOptions, } from '../platform/space/index.js';
|
|
18
|
+
export { type JoinSelfOptions, teamAppExtensions } from '../platform/team-app/index.js';
|
|
17
19
|
export { type GetTeamWithDelinquencyOptions, teamExtensions, type TeamWithDelinquency } from '../platform/team.js';
|
|
18
20
|
export { telemetryDrainExtensions, type TelemetryDrainOptions, type TelemetryDrainTarget } from '../platform/telemetry-drain/index.js';
|
|
19
21
|
export { testRunExtensions, TestRunNotReadyError, type TestRunStatus, waitForState, type WaitForStateOptions, } from '../platform/test-run/index.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { accountExtensions } from '../platform/account.js';
|
|
2
2
|
export { addOnAttachmentExtensions } from '../platform/add-on-attachment/index.js';
|
|
3
3
|
export { addOnExtensions } from '../platform/add-on/index.js';
|
|
4
|
+
export { appCollaboratorExtensions, } from '../platform/app-collaborator/index.js';
|
|
4
5
|
export { appExtensions, } from '../platform/app/index.js';
|
|
5
6
|
export { containerExtensions, NotAContainerAppError, } from '../platform/container/index.js';
|
|
6
7
|
export { formatDelinquencyWarning, } from '../platform/delinquency.js';
|
|
@@ -14,6 +15,7 @@ export { reviewAppConfigExtensions } from '../platform/review-app-config/index.j
|
|
|
14
15
|
export { privateToShield, shieldToPrivate } from '../platform/shield.js';
|
|
15
16
|
export { createAndAssociate, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
|
|
16
17
|
export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, waitForAllocated, } from '../platform/space/index.js';
|
|
18
|
+
export { teamAppExtensions } from '../platform/team-app/index.js';
|
|
17
19
|
export { teamExtensions } from '../platform/team.js';
|
|
18
20
|
export { telemetryDrainExtensions } from '../platform/telemetry-drain/index.js';
|
|
19
21
|
export { testRunExtensions, TestRunNotReadyError, waitForState, } from '../platform/test-run/index.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RemoveSelfOptions } from './remove-self.js';
|
|
2
|
+
export { type BeforeRemove, removeSelf, type RemoveSelfOptions } from './remove-self.js';
|
|
3
|
+
export declare const appCollaboratorExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "appCollaborator", {
|
|
4
|
+
removeSelf: (appIdentity: string, options?: RemoveSelfOptions) => Promise<import("@heroku/types/3.sdk").Collaborator>;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { removeSelf } from './remove-self.js';
|
|
3
|
+
export { removeSelf } from './remove-self.js';
|
|
4
|
+
export const appCollaboratorExtensions = extendResource('platform', 'appCollaborator', ctx => ({
|
|
5
|
+
removeSelf: (appIdentity, options) => removeSelf(ctx, appIdentity, options),
|
|
6
|
+
}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Collaborator } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
import type { AccountWithDelinquency } from '../account.js';
|
|
4
|
+
export type BeforeRemove = (details: AccountWithDelinquency) => Promise<void> | void;
|
|
5
|
+
export type RemoveSelfOptions = {
|
|
6
|
+
beforeRemove?: BeforeRemove;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
};
|
|
9
|
+
/** Remove the authenticated account from an app's collaborator list. */
|
|
10
|
+
export declare function removeSelf(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: RemoveSelfOptions): Promise<Collaborator>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
import { getAccountWithDelinquency } from '../account.js';
|
|
3
|
+
/** Remove the authenticated account from an app's collaborator list. */
|
|
4
|
+
export async function removeSelf(ctx, appIdentity, options = {}) {
|
|
5
|
+
options.signal?.throwIfAborted();
|
|
6
|
+
let account;
|
|
7
|
+
if (options.beforeRemove) {
|
|
8
|
+
const details = await getAccountWithDelinquency(ctx, options.signal ? { signal: options.signal } : {});
|
|
9
|
+
options.signal?.throwIfAborted();
|
|
10
|
+
await options.beforeRemove(details);
|
|
11
|
+
options.signal?.throwIfAborted();
|
|
12
|
+
account = details.account;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
16
|
+
account = await platform.account.info();
|
|
17
|
+
options.signal?.throwIfAborted();
|
|
18
|
+
}
|
|
19
|
+
// Keep the email raw: the generated route encodes path parameters once.
|
|
20
|
+
return withSignal(ctx.platform, options.signal).collaborator.delete(appIdentity, account.email);
|
|
21
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { JoinSelfOptions } from './join-self.js';
|
|
2
|
+
export { joinSelf, type JoinSelfOptions } from './join-self.js';
|
|
3
|
+
export declare const teamAppExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "teamApp", {
|
|
4
|
+
joinSelf: (appIdentity: string, options?: JoinSelfOptions) => Promise<import("@heroku/types/3.sdk").TeamAppCollaborator>;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { extendResource } from '../../../core/extend-resource.js';
|
|
2
|
+
import { joinSelf } from './join-self.js';
|
|
3
|
+
export { joinSelf } from './join-self.js';
|
|
4
|
+
export const teamAppExtensions = extendResource('platform', 'teamApp', ctx => ({
|
|
5
|
+
joinSelf: (appIdentity, options) => joinSelf(ctx, appIdentity, options),
|
|
6
|
+
}));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TeamAppCollaborator } from '@heroku/types/3.sdk';
|
|
2
|
+
import type { ResourceCtx } from '../../../core/extend-resource.js';
|
|
3
|
+
export type JoinSelfOptions = {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
};
|
|
6
|
+
/** Add the authenticated account as a collaborator on a team app. */
|
|
7
|
+
export declare function joinSelf(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: JoinSelfOptions): Promise<TeamAppCollaborator>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { withSignal } from '../../../core/with-signal.js';
|
|
2
|
+
/** Add the authenticated account as a collaborator on a team app. */
|
|
3
|
+
export async function joinSelf(ctx, appIdentity, options = {}) {
|
|
4
|
+
options.signal?.throwIfAborted();
|
|
5
|
+
const platform = withSignal(ctx.platform, options.signal);
|
|
6
|
+
const account = await platform.account.info();
|
|
7
|
+
options.signal?.throwIfAborted();
|
|
8
|
+
return platform.teamAppCollaborator.create(appIdentity, { user: account.email });
|
|
9
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.12.
|
|
1
|
+
export declare const SDK_VERSION = "0.12.2";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/write-version.mjs (build prebuild). Do not edit.
|
|
2
|
-
export const SDK_VERSION = '0.12.
|
|
2
|
+
export const SDK_VERSION = '0.12.2';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroku/sdk",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Heroku SDK",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"type": "module",
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
"types": "./dist/resources/extensions/platform.d.ts",
|
|
43
43
|
"default": "./dist/resources/extensions/platform.js"
|
|
44
44
|
},
|
|
45
|
+
"./extensions/dashboard-backend": {
|
|
46
|
+
"types": "./dist/resources/extensions/dashboard-backend.d.ts",
|
|
47
|
+
"default": "./dist/resources/extensions/dashboard-backend.js"
|
|
48
|
+
},
|
|
45
49
|
"./extensions/data": {
|
|
46
50
|
"types": "./dist/resources/extensions/data.d.ts",
|
|
47
51
|
"default": "./dist/resources/extensions/data.js"
|
|
@@ -102,6 +106,10 @@
|
|
|
102
106
|
"types": "./dist/resources/platform/vpn-connection/index.d.ts",
|
|
103
107
|
"default": "./dist/resources/platform/vpn-connection/index.js"
|
|
104
108
|
},
|
|
109
|
+
"./resources/data/database-credential": {
|
|
110
|
+
"types": "./dist/resources/data/database-credential/index.d.ts",
|
|
111
|
+
"default": "./dist/resources/data/database-credential/index.js"
|
|
112
|
+
},
|
|
105
113
|
"./resources/data/redis": {
|
|
106
114
|
"types": "./dist/resources/data/redis/index.d.ts",
|
|
107
115
|
"default": "./dist/resources/data/redis/index.js"
|
|
@@ -149,14 +157,14 @@
|
|
|
149
157
|
"@heroku-cli/test-utils": "^1.0.3",
|
|
150
158
|
"@types/debug": "^4.1.12",
|
|
151
159
|
"@types/node": "^26.5.1",
|
|
152
|
-
"@vitest/coverage-v8": "^
|
|
160
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
153
161
|
"@vitest/eslint-plugin": "^1.6.0",
|
|
154
162
|
"eslint": "^9.39.4",
|
|
155
163
|
"eslint-config-oclif": "^6.0.167",
|
|
156
164
|
"rimraf": "^6.1.3",
|
|
157
165
|
"tsx": "^4.22.4",
|
|
158
166
|
"typescript": "^6.0.2",
|
|
159
|
-
"vitest": "^
|
|
167
|
+
"vitest": "^5.0.5"
|
|
160
168
|
},
|
|
161
169
|
"dependencies": {
|
|
162
170
|
"@heroku/heroku-fetch": "^0.2.3",
|