@heroku/sdk 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +16 -8
  2. package/dist/core/extend-resource.d.ts +5 -1
  3. package/dist/core/heroku-sdk.d.ts +4 -0
  4. package/dist/core/heroku-sdk.js +30 -0
  5. package/dist/resources/extensions/platform.d.ts +3 -1
  6. package/dist/resources/extensions/platform.js +3 -1
  7. package/dist/resources/platform/app/diff.d.ts +20 -0
  8. package/dist/resources/platform/app/diff.js +110 -0
  9. package/dist/resources/platform/app/index.d.ts +3 -0
  10. package/dist/resources/platform/app/index.js +3 -0
  11. package/dist/resources/platform/container/ensure-container-stack.d.ts +2 -7
  12. package/dist/resources/platform/container/ensure-container-stack.js +1 -10
  13. package/dist/resources/platform/domain/test-utils.js +2 -0
  14. package/dist/resources/platform/space/errors.d.ts +28 -0
  15. package/dist/resources/platform/space/errors.js +34 -0
  16. package/dist/resources/platform/space/index.d.ts +7 -0
  17. package/dist/resources/platform/space/index.js +7 -0
  18. package/dist/resources/platform/space/types.d.ts +25 -0
  19. package/dist/resources/platform/space/types.js +1 -0
  20. package/dist/resources/platform/space/wait-for-allocated.d.ts +29 -0
  21. package/dist/resources/platform/space/wait-for-allocated.js +56 -0
  22. package/dist/resources/platform/vpn-connection/index.d.ts +8 -0
  23. package/dist/resources/platform/vpn-connection/index.js +6 -0
  24. package/dist/resources/platform/vpn-connection/wait-for-active.d.ts +39 -0
  25. package/dist/resources/platform/vpn-connection/wait-for-active.js +48 -0
  26. package/dist/services/dashboard-backend.d.ts +5 -0
  27. package/dist/services/dashboard-backend.js +10 -0
  28. package/dist/services/repositories.d.ts +5 -0
  29. package/dist/services/repositories.js +11 -0
  30. package/package.json +30 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Heroku SDK
2
2
 
3
- A TypeScript SDK for the [Heroku Platform API](https://devcenter.heroku.com/articles/platform-api-reference), [Heroku Data API](https://devcenter.heroku.com/articles/heroku-postgres-api-reference), and the Heroku Metrics API. It generates fully-typed clients at runtime from route definitions — no hand-written method per endpoint.
3
+ A TypeScript SDK for the [Heroku Platform API](https://devcenter.heroku.com/articles/platform-api-reference), [Heroku Data API](https://devcenter.heroku.com/articles/heroku-postgres-api-reference), the Heroku Dashboard Backend, the Heroku Metrics API, and the Heroku Repositories API. It generates fully-typed clients at runtime from route definitions — no hand-written method per endpoint.
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,6 +22,7 @@ const sdk = new HerokuSDK({ extensions: [appExtensions] })
22
22
  // Upstream route (auto-generated from the API spec)
23
23
  const apps = await sdk.platform.app.list()
24
24
  const app = await sdk.platform.app.info('my-app')
25
+ const favorites = await sdk.dashboardBackend.favorite.list({ type: 'app' })
25
26
 
26
27
  // Hand-written extension method (provided by appExtensions)
27
28
  await sdk.platform.app.enableMaintenance('my-app')
@@ -38,15 +39,20 @@ const sdk = new HerokuSDK({ clientOptions: { token: 'your-api-token' } })
38
39
  If you only need one service (and want the smallest possible bundle), import the factory directly. These return the same typed client the SDK class wraps internally.
39
40
 
40
41
  ```ts
41
- import { createPlatformClient } from '@heroku/sdk/platform'
42
- import { createDataClient } from '@heroku/sdk/data'
43
- import { createMetricsClient } from '@heroku/sdk/metrics'
44
-
45
- const platform = createPlatformClient()
46
- const data = createDataClient()
47
- const metrics = createMetricsClient()
42
+ import { createPlatformClient } from '@heroku/sdk/platform'
43
+ import { createDataClient } from '@heroku/sdk/data'
44
+ import { createDashboardBackendClient } from '@heroku/sdk/dashboard-backend'
45
+ import { createMetricsClient } from '@heroku/sdk/metrics'
46
+ import { createRepositoriesClient } from '@heroku/sdk/repositories'
47
+
48
+ const platform = createPlatformClient()
49
+ const data = createDataClient()
50
+ const dashboardBackend = createDashboardBackendClient()
51
+ const metrics = createMetricsClient()
52
+ const repositories = createRepositoriesClient()
48
53
 
49
54
  const apps = await platform.app.list()
55
+ const favorites = await dashboardBackend.favorite.list({ type: 'app' })
50
56
  ```
51
57
 
52
58
  ### Imports
@@ -56,7 +62,9 @@ const apps = await platform.app.list()
56
62
  | `HerokuSDK`, `extendResource`, types | `@heroku/sdk` |
57
63
  | `createPlatformClient`, `PlatformClient` | `@heroku/sdk/platform` |
58
64
  | `createDataClient`, `DataClient` | `@heroku/sdk/data` |
65
+ | `createDashboardBackendClient`, `DashboardBackendClient` | `@heroku/sdk/dashboard-backend` |
59
66
  | `createMetricsClient`, `MetricsClient` | `@heroku/sdk/metrics` |
67
+ | `createRepositoriesClient`, `RepositoriesClient` | `@heroku/sdk/repositories` |
60
68
  | `appExtensions`, `dynoExtensions`, … | `@heroku/sdk/extensions/platform` |
61
69
  | `databaseExtensions`, … | `@heroku/sdk/extensions/data` |
62
70
 
@@ -1,11 +1,15 @@
1
+ import type { DashboardBackendClient } from '../services/dashboard-backend.js';
1
2
  import type { DataClient } from '../services/data.js';
2
3
  import type { MetricsClient } from '../services/metrics.js';
3
4
  import type { PlatformClient } from '../services/platform.js';
4
- export type ServiceName = 'data' | 'metrics' | 'platform';
5
+ import type { RepositoriesClient } from '../services/repositories.js';
6
+ export type ServiceName = 'dashboardBackend' | 'data' | 'metrics' | 'platform' | 'repositories';
5
7
  export type ResourceCtx = {
8
+ dashboardBackend: DashboardBackendClient;
6
9
  data: DataClient;
7
10
  metrics: MetricsClient;
8
11
  platform: PlatformClient;
12
+ repositories: RepositoriesClient;
9
13
  };
10
14
  export type ResourceMethods = Record<string, (...args: any[]) => any>;
11
15
  export type ResourceExtension<S extends ServiceName = ServiceName, R extends string = string, M extends ResourceMethods = ResourceMethods> = {
@@ -1,7 +1,9 @@
1
1
  import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { DashboardBackendClient } from '../services/dashboard-backend.js';
2
3
  import type { DataClient } from '../services/data.js';
3
4
  import type { MetricsClient } from '../services/metrics.js';
4
5
  import type { PlatformClient } from '../services/platform.js';
6
+ import type { RepositoriesClient } from '../services/repositories.js';
5
7
  import type { ApplyExtensions, ExtensionsFor, ResourceExtension } from './extend-resource.js';
6
8
  export type HerokuSDKOptions<Exts extends readonly ResourceExtension[]> = {
7
9
  clientOptions?: HerokuApiClientOptions;
@@ -10,7 +12,9 @@ export type HerokuSDKOptions<Exts extends readonly ResourceExtension[]> = {
10
12
  export declare class HerokuSDK<const Exts extends readonly ResourceExtension[] = readonly ResourceExtension[]> {
11
13
  #private;
12
14
  constructor(options?: HerokuSDKOptions<Exts>);
15
+ get dashboardBackend(): ApplyExtensions<DashboardBackendClient, ExtensionsFor<Exts, 'dashboardBackend'>>;
13
16
  get data(): ApplyExtensions<DataClient, ExtensionsFor<Exts, 'data'>>;
14
17
  get metrics(): ApplyExtensions<MetricsClient, ExtensionsFor<Exts, 'metrics'>>;
15
18
  get platform(): ApplyExtensions<PlatformClient, ExtensionsFor<Exts, 'platform'>>;
19
+ get repositories(): ApplyExtensions<RepositoriesClient, ExtensionsFor<Exts, 'repositories'>>;
16
20
  }
@@ -1,6 +1,8 @@
1
+ import { createDashboardBackendClient } from '../services/dashboard-backend.js';
1
2
  import { createDataClient } from '../services/data.js';
2
3
  import { createMetricsClient } from '../services/metrics.js';
3
4
  import { createPlatformClient } from '../services/platform.js';
5
+ import { createRepositoriesClient } from '../services/repositories.js';
4
6
  import { mergeExtensions } from './extensions-proxy.js';
5
7
  function partitionByService(extensions) {
6
8
  const map = new Map();
@@ -14,17 +16,25 @@ function partitionByService(extensions) {
14
16
  export class HerokuSDK {
15
17
  #clientOptions;
16
18
  #ctx;
19
+ #dashboardBackend;
17
20
  #data;
18
21
  #extensionsByService;
19
22
  #metrics;
20
23
  #platform;
24
+ #rawDashboardBackend;
21
25
  #rawData;
22
26
  #rawMetrics;
23
27
  #rawPlatform;
28
+ #rawRepositories;
29
+ #repositories;
24
30
  constructor(options = {}) {
25
31
  this.#clientOptions = options.clientOptions ?? {};
26
32
  this.#extensionsByService = partitionByService(options.extensions ?? []);
27
33
  }
34
+ get dashboardBackend() {
35
+ this.#dashboardBackend ??= mergeExtensions(this.#getRawDashboardBackend(), this.#extensionsByService.get('dashboardBackend') ?? [], this.#getCtx());
36
+ return this.#dashboardBackend;
37
+ }
28
38
  get data() {
29
39
  this.#data ??= mergeExtensions(this.#getRawData(), this.#extensionsByService.get('data') ?? [], this.#getCtx());
30
40
  return this.#data;
@@ -37,8 +47,16 @@ export class HerokuSDK {
37
47
  this.#platform ??= mergeExtensions(this.#getRawPlatform(), this.#extensionsByService.get('platform') ?? [], this.#getCtx());
38
48
  return this.#platform;
39
49
  }
50
+ get repositories() {
51
+ this.#repositories ??= mergeExtensions(this.#getRawRepositories(), this.#extensionsByService.get('repositories') ?? [], this.#getCtx());
52
+ return this.#repositories;
53
+ }
40
54
  #getCtx() {
41
55
  this.#ctx ??= Object.defineProperties({}, {
56
+ dashboardBackend: {
57
+ enumerable: true,
58
+ get: () => this.#getRawDashboardBackend(),
59
+ },
42
60
  data: {
43
61
  enumerable: true,
44
62
  get: () => this.#getRawData(),
@@ -51,9 +69,17 @@ export class HerokuSDK {
51
69
  enumerable: true,
52
70
  get: () => this.#getRawPlatform(),
53
71
  },
72
+ repositories: {
73
+ enumerable: true,
74
+ get: () => this.#getRawRepositories(),
75
+ },
54
76
  });
55
77
  return this.#ctx;
56
78
  }
79
+ #getRawDashboardBackend() {
80
+ this.#rawDashboardBackend ??= createDashboardBackendClient(this.#clientOptions);
81
+ return this.#rawDashboardBackend;
82
+ }
57
83
  #getRawData() {
58
84
  this.#rawData ??= createDataClient(this.#clientOptions);
59
85
  return this.#rawData;
@@ -66,4 +92,8 @@ export class HerokuSDK {
66
92
  this.#rawPlatform ??= createPlatformClient(this.#clientOptions);
67
93
  return this.#rawPlatform;
68
94
  }
95
+ #getRawRepositories() {
96
+ this.#rawRepositories ??= createRepositoriesClient(this.#clientOptions);
97
+ return this.#rawRepositories;
98
+ }
69
99
  }
@@ -1,5 +1,5 @@
1
1
  export { addOnExtensions } from '../platform/add-on/index.js';
2
- export { appExtensions, type AppInfo, type PipelineCouplingDetail } from '../platform/app/index.js';
2
+ export { appExtensions, type AppInfo, type DiffAppsOptions, type DiffRow, type PipelineCouplingDetail, } from '../platform/app/index.js';
3
3
  export { containerExtensions, type DockerReleasesBatchUpdateOpts, NotAContainerAppError, type ReleaseDockerImagesResult, type RemoveProcessTypesOpts, } from '../platform/container/index.js';
4
4
  export { type CreateAndWaitOptions, domainExtensions, type DomainOptions, type WaitForReadyOptions, } from '../platform/domain/index.js';
5
5
  export { dynoExtensions, DynoNotReadyError, type DynoState, runDyno, type RunDynoOptions, waitForInfo, type WaitForInfoOptions, } from '../platform/dyno/index.js';
@@ -9,5 +9,7 @@ export { pipelineExtensions } from '../platform/pipeline/index.js';
9
9
  export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
10
10
  export { privateToShield, shieldToPrivate } from '../platform/shield.js';
11
11
  export { createAndAssociate, type CreateAndAssociateOptions, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
12
+ export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, type SpaceOptions, type SpaceWithNat, waitForAllocated, type WaitForAllocatedOptions, } from '../platform/space/index.js';
12
13
  export { telemetryDrainExtensions, type TelemetryDrainOptions, type TelemetryDrainTarget } from '../platform/telemetry-drain/index.js';
13
14
  export { testRunExtensions, TestRunNotReadyError, type TestRunStatus, waitForState, type WaitForStateOptions, } from '../platform/test-run/index.js';
15
+ export { vpnConnectionExtensions, VpnConnectionFailedError, VpnConnectionNotReadyError, type VpnConnectionOptions, waitForActive, type WaitForActiveOptions, } from '../platform/vpn-connection/index.js';
@@ -1,5 +1,5 @@
1
1
  export { addOnExtensions } from '../platform/add-on/index.js';
2
- export { appExtensions } from '../platform/app/index.js';
2
+ export { appExtensions, } from '../platform/app/index.js';
3
3
  export { containerExtensions, NotAContainerAppError, } from '../platform/container/index.js';
4
4
  export { domainExtensions, } from '../platform/domain/index.js';
5
5
  export { dynoExtensions, DynoNotReadyError, runDyno, waitForInfo, } from '../platform/dyno/index.js';
@@ -9,5 +9,7 @@ export { pipelineExtensions } from '../platform/pipeline/index.js';
9
9
  export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
10
10
  export { privateToShield, shieldToPrivate } from '../platform/shield.js';
11
11
  export { createAndAssociate, sniEndpointExtensions } from '../platform/sni-endpoint/index.js';
12
+ export { spaceExtensions, SpaceNotAllocatedError, SpaceNotReadyError, waitForAllocated, } from '../platform/space/index.js';
12
13
  export { telemetryDrainExtensions } from '../platform/telemetry-drain/index.js';
13
14
  export { testRunExtensions, TestRunNotReadyError, waitForState, } from '../platform/test-run/index.js';
15
+ export { vpnConnectionExtensions, VpnConnectionFailedError, VpnConnectionNotReadyError, waitForActive, } from '../platform/vpn-connection/index.js';
@@ -0,0 +1,20 @@
1
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
2
+ export type DiffAppsOptions = {
3
+ signal?: AbortSignal;
4
+ };
5
+ export interface DiffRow {
6
+ app1: string | undefined;
7
+ app2: string | undefined;
8
+ prop: string;
9
+ }
10
+ /**
11
+ * Composite diff of two apps: compares slug checksum, config vars, stack,
12
+ * buildpacks, add-ons, and features, returning one `DiffRow` per differing
13
+ * property.
14
+ *
15
+ * Faithful port of the Heroku CLI's `apps:diff`: `diffFiles` runs first (so a
16
+ * missing app surfaces the `App not found: <app>` error before the rest), then
17
+ * the remaining five diffs run in parallel and their rows are concatenated in
18
+ * the CLI's order. Rendering (truncation, table output) stays with the caller.
19
+ */
20
+ export declare function diffApps(ctx: Pick<ResourceCtx, 'platform'>, app1: string, app2: string, options?: DiffAppsOptions): Promise<DiffRow[]>;
@@ -0,0 +1,110 @@
1
+ import { NotFoundError } from '@heroku/heroku-fetch';
2
+ async function checksum(platform, app) {
3
+ try {
4
+ const releases = await platform.withHeaders({ Range: 'version ..; max=1, order=desc' }).release.list(app);
5
+ if (releases?.[0]?.slug) {
6
+ const slugId = releases[0].slug.id;
7
+ const slug = await platform.slug.info(app, slugId);
8
+ return slug?.checksum ?? null;
9
+ }
10
+ return null;
11
+ }
12
+ catch (error) {
13
+ if (isNotFound(error)) {
14
+ throw new Error(`App not found: ${app}`, { cause: error });
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+ function isNotFound(error) {
20
+ return error instanceof NotFoundError;
21
+ }
22
+ async function diffFiles(platform, app1, app2) {
23
+ const sums = await Promise.all([checksum(platform, app1), checksum(platform, app2)]);
24
+ return sums[0] === sums[1] ? [] : [{ app1: sums[0] ?? undefined, app2: sums[1] ?? undefined, prop: 'slug (checksum)' }];
25
+ }
26
+ async function diffEnv(platform, app1, app2) {
27
+ const [vars1, vars2] = await Promise.all([
28
+ platform.configVar.infoForApp(app1),
29
+ platform.configVar.infoForApp(app2),
30
+ ]);
31
+ const cfg1 = vars1 ?? {};
32
+ const cfg2 = vars2 ?? {};
33
+ const keys = new Set([...Object.keys(cfg1), ...Object.keys(cfg2)]);
34
+ return [...keys]
35
+ .filter(k => cfg1[k] !== cfg2[k])
36
+ .map(k => ({ app1: cfg1[k], app2: cfg2[k], prop: `config (${k})` }));
37
+ }
38
+ async function diffStack(platform, app1, app2) {
39
+ const [res1, res2] = await Promise.all([
40
+ platform.app.info(app1),
41
+ platform.app.info(app2),
42
+ ]);
43
+ const a = res1?.stack?.name;
44
+ const b = res2?.stack?.name;
45
+ return a === b ? [] : [{ app1: a, app2: b, prop: 'stack' }];
46
+ }
47
+ async function diffBuildpacks(platform, app1, app2) {
48
+ const [res1, res2] = await Promise.all([
49
+ platform.buildpackInstallation.list(app1),
50
+ platform.buildpackInstallation.list(app2),
51
+ ]);
52
+ const bps1 = res1 ?? [];
53
+ const bps2 = res2 ?? [];
54
+ const urls1 = bps1.map(obj => obj.buildpack?.url ?? '');
55
+ const urls2 = bps2.map(obj => obj.buildpack?.url ?? '');
56
+ const longest = urls1.length >= urls2.length ? urls1 : urls2;
57
+ const pairs = longest.map((_, k) => ({
58
+ app1: urls1[k],
59
+ app2: urls2[k],
60
+ prop: `buildpack (${k})`,
61
+ }));
62
+ return pairs.filter(pair => pair.app1 !== pair.app2);
63
+ }
64
+ async function diffAddons(platform, app1, app2) {
65
+ const [addons1, addons2] = await Promise.all([
66
+ platform.addOn.listByApp(app1),
67
+ platform.addOn.listByApp(app2),
68
+ ]);
69
+ const names1 = new Set((addons1 ?? []).map(addon => addon.addon_service?.name ?? '').filter(Boolean));
70
+ const names2 = new Set((addons2 ?? []).map(addon => addon.addon_service?.name ?? '').filter(Boolean));
71
+ const only1 = [...names1].filter(name => !names2.has(name)).map(name => ({ app1: 'true', app2: 'false', prop: `add-on (${name})` }));
72
+ const only2 = [...names2].filter(name => !names1.has(name)).map(name => ({ app1: 'false', app2: 'true', prop: `add-on (${name})` }));
73
+ return [...only1, ...only2];
74
+ }
75
+ async function diffFeatures(platform, app1, app2) {
76
+ const [features1, features2] = await Promise.all([
77
+ platform.appFeature.list(app1),
78
+ platform.appFeature.list(app2),
79
+ ]);
80
+ const names1 = new Set((features1 ?? []).map(f => (f.enabled ? f.name : null)).filter(Boolean));
81
+ const names2 = new Set((features2 ?? []).map(f => (f.enabled ? f.name : null)).filter(Boolean));
82
+ const only1 = [...names1].filter(name => !names2.has(name)).map(name => ({ app1: 'enabled', app2: 'disabled', prop: `feature (${name})` }));
83
+ const only2 = [...names2].filter(name => !names1.has(name)).map(name => ({ app1: 'disabled', app2: 'enabled', prop: `feature (${name})` }));
84
+ return [...only1, ...only2];
85
+ }
86
+ /**
87
+ * Composite diff of two apps: compares slug checksum, config vars, stack,
88
+ * buildpacks, add-ons, and features, returning one `DiffRow` per differing
89
+ * property.
90
+ *
91
+ * Faithful port of the Heroku CLI's `apps:diff`: `diffFiles` runs first (so a
92
+ * missing app surfaces the `App not found: <app>` error before the rest), then
93
+ * the remaining five diffs run in parallel and their rows are concatenated in
94
+ * the CLI's order. Rendering (truncation, table output) stays with the caller.
95
+ */
96
+ export async function diffApps(ctx, app1, app2, options = {}) {
97
+ options.signal?.throwIfAborted();
98
+ const platform = options.signal
99
+ ? ctx.platform.withOptions({ signal: options.signal })
100
+ : ctx.platform;
101
+ const files = await diffFiles(platform, app1, app2);
102
+ const [env, stack, bp, addons, features] = await Promise.all([
103
+ diffEnv(platform, app1, app2),
104
+ diffStack(platform, app1, app2),
105
+ diffBuildpacks(platform, app1, app2),
106
+ diffAddons(platform, app1, app2),
107
+ diffFeatures(platform, app1, app2),
108
+ ]);
109
+ return [...files, ...env, ...stack, ...bp, ...addons, ...features];
110
+ }
@@ -1,10 +1,12 @@
1
1
  import type { App } from '@heroku/types/3.sdk';
2
2
  import type { ResourceCtx } from '../../../core/extend-resource.js';
3
3
  import type { CreateAndSetupInput, CreateAndSetupOptions } from './create-and-setup.js';
4
+ import type { DiffAppsOptions } from './diff.js';
4
5
  import type { DescribeAppOptions } from './info.js';
5
6
  import type { TransferOptions } from './transfer.js';
6
7
  import type { WaitForACMCertificatesOptions } from './wait-for-acm-certificates.js';
7
8
  export { createAndSetup, type CreateAndSetupInput, type CreateAndSetupOptions, type SetupStep, } from './create-and-setup.js';
9
+ export { diffApps, type DiffAppsOptions, type DiffRow } from './diff.js';
8
10
  export { type AppInfo, describeApp, type DescribeAppOptions, type PipelineCouplingDetail, } from './info.js';
9
11
  export { transferApp, type TransferOptions } from './transfer.js';
10
12
  export { waitForACMCertificates, type WaitForACMCertificatesOptions } from './wait-for-acm-certificates.js';
@@ -57,6 +59,7 @@ export declare function getProcessTier(ctx: {
57
59
  export declare const appExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "app", {
58
60
  createAndSetup: (input: CreateAndSetupInput, options?: CreateAndSetupOptions) => Promise<App>;
59
61
  describe: (appIdentity: string, options?: DescribeAppOptions) => Promise<import("./info.js").AppInfo>;
62
+ diff: (app1: string, app2: string, options?: DiffAppsOptions) => Promise<import("./diff.js").DiffRow[]>;
60
63
  disableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
61
64
  enableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
62
65
  getGeneration: (appIdentity: string, options?: GetGenerationOptions) => Promise<GenerationKind | undefined>;
@@ -1,9 +1,11 @@
1
1
  import { extendResource } from '../../../core/extend-resource.js';
2
2
  import { createAndSetup } from './create-and-setup.js';
3
+ import { diffApps } from './diff.js';
3
4
  import { describeApp } from './info.js';
4
5
  import { transferApp } from './transfer.js';
5
6
  import { waitForACMCertificates } from './wait-for-acm-certificates.js';
6
7
  export { createAndSetup, } from './create-and-setup.js';
8
+ export { diffApps } from './diff.js';
7
9
  export { describeApp, } from './info.js';
8
10
  export { transferApp } from './transfer.js';
9
11
  export { waitForACMCertificates } from './wait-for-acm-certificates.js';
@@ -52,6 +54,7 @@ export async function getProcessTier(ctx, appIdentity, options = {}) {
52
54
  export const appExtensions = extendResource('platform', 'app', ctx => ({
53
55
  createAndSetup: (input, options) => createAndSetup(ctx, input, options),
54
56
  describe: (appIdentity, options) => describeApp(ctx, appIdentity, options),
57
+ diff: (app1, app2, options) => diffApps(ctx, app1, app2, options),
55
58
  disableMaintenance: (appIdentity, options) => disableMaintenance(ctx, appIdentity, options),
56
59
  enableMaintenance: (appIdentity, options) => enableMaintenance(ctx, appIdentity, options),
57
60
  getGeneration: (appIdentity, options) => getGeneration(ctx, appIdentity, options),
@@ -2,14 +2,9 @@ import type { App } from '@heroku/types/3.sdk';
2
2
  import type { ResourceCtx } from '../../../core/extend-resource.js';
3
3
  import type { ContainerOptions } from './index.js';
4
4
  export declare class NotAContainerAppError extends Error {
5
- readonly app: Pick<App, 'build_stack' | 'id' | 'name' | 'stack'>;
5
+ readonly app: App;
6
6
  readonly id = "not_a_container_app";
7
- constructor(app: Pick<App, 'build_stack' | 'id' | 'name' | 'stack'>);
8
- get body(): {
9
- app: Pick<App, "stack" | "id" | "name" | "build_stack">;
10
- id: string;
11
- message: string;
12
- };
7
+ constructor(app: App);
13
8
  }
14
9
  /**
15
10
  * Ensure that the given app is a container app.
@@ -6,9 +6,6 @@ export class NotAContainerAppError extends Error {
6
6
  this.app = app;
7
7
  this.name = 'NotAContainerAppError';
8
8
  }
9
- get body() {
10
- return { app: this.app, id: this.id, message: this.message };
11
- }
12
9
  }
13
10
  /**
14
11
  * Ensure that the given app is a container app.
@@ -21,12 +18,6 @@ export async function ensureContainerStack(ctx, appIdentity, options = {}) {
21
18
  const app = await platform.app.info(appIdentity);
22
19
  const allowedStack = 'container';
23
20
  if (app.build_stack.name !== allowedStack && app.stack.name !== allowedStack) {
24
- throw new NotAContainerAppError({
25
- // eslint-disable-next-line camelcase
26
- build_stack: app.build_stack,
27
- id: app.id,
28
- name: app.name,
29
- stack: app.stack,
30
- });
21
+ throw new NotAContainerAppError(app);
31
22
  }
32
23
  }
@@ -16,9 +16,11 @@ export function buildCtx(stubs = {}) {
16
16
  };
17
17
  platform.withOptions.mockReturnValue(platform);
18
18
  return {
19
+ dashboardBackend: {},
19
20
  data: {},
20
21
  metrics: {},
21
22
  platform: platform,
23
+ repositories: {},
22
24
  };
23
25
  }
24
26
  export function buildDomain(overrides = {}) {
@@ -0,0 +1,28 @@
1
+ import type { Space } from '@heroku/types/3.sdk';
2
+ /**
3
+ * Thrown by `waitForAllocated` when the space settles into a terminal
4
+ * state other than `allocated` (e.g. `deleting`).
5
+ */
6
+ export declare class SpaceNotAllocatedError extends Error {
7
+ readonly space: Space;
8
+ readonly id = "space_not_allocated";
9
+ constructor(space: Space);
10
+ get body(): {
11
+ id: string;
12
+ message: string;
13
+ };
14
+ }
15
+ /**
16
+ * Thrown by `waitForAllocated` when `timeoutMs` elapses while the space
17
+ * is still `allocating`.
18
+ */
19
+ export declare class SpaceNotReadyError extends Error {
20
+ readonly space: Space;
21
+ readonly timeoutMs: number;
22
+ readonly id = "space_not_ready";
23
+ constructor(space: Space, timeoutMs: number);
24
+ get body(): {
25
+ id: string;
26
+ message: string;
27
+ };
28
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Thrown by `waitForAllocated` when the space settles into a terminal
3
+ * state other than `allocated` (e.g. `deleting`).
4
+ */
5
+ export class SpaceNotAllocatedError extends Error {
6
+ space;
7
+ id = 'space_not_allocated';
8
+ constructor(space) {
9
+ super(`The space ${space.name} did not become allocated, with state ${space.state}.`);
10
+ this.space = space;
11
+ this.name = 'SpaceNotAllocatedError';
12
+ }
13
+ get body() {
14
+ return { id: this.id, message: this.message };
15
+ }
16
+ }
17
+ /**
18
+ * Thrown by `waitForAllocated` when `timeoutMs` elapses while the space
19
+ * is still `allocating`.
20
+ */
21
+ export class SpaceNotReadyError extends Error {
22
+ space;
23
+ timeoutMs;
24
+ id = 'space_not_ready';
25
+ constructor(space, timeoutMs) {
26
+ super(`Timeout waiting for space ${space.name} to become allocated.`);
27
+ this.space = space;
28
+ this.timeoutMs = timeoutMs;
29
+ this.name = 'SpaceNotReadyError';
30
+ }
31
+ get body() {
32
+ return { id: this.id, message: this.message };
33
+ }
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { WaitForAllocatedOptions } from './types.js';
2
+ export { SpaceNotAllocatedError, SpaceNotReadyError } from './errors.js';
3
+ export type { SpaceOptions, WaitForAllocatedOptions } from './types.js';
4
+ export { type SpaceWithNat, waitForAllocated } from './wait-for-allocated.js';
5
+ export declare const spaceExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "space", {
6
+ waitForAllocated: (name: string, options?: WaitForAllocatedOptions) => Promise<import("./wait-for-allocated.js").SpaceWithNat>;
7
+ }>;
@@ -0,0 +1,7 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { waitForAllocated } from './wait-for-allocated.js';
3
+ export { SpaceNotAllocatedError, SpaceNotReadyError } from './errors.js';
4
+ export { waitForAllocated } from './wait-for-allocated.js';
5
+ export const spaceExtensions = extendResource('platform', 'space', ctx => ({
6
+ waitForAllocated: (name, options) => waitForAllocated(ctx, name, options),
7
+ }));
@@ -0,0 +1,25 @@
1
+ export type SpaceOptions = {
2
+ signal?: AbortSignal;
3
+ };
4
+ export type WaitForAllocatedOptions = SpaceOptions & {
5
+ /**
6
+ * When true, once the space reaches a terminal state, also fetches
7
+ * the space's NAT info (`spaceNat.info`) and attaches it as `nat` on
8
+ * the result.
9
+ *
10
+ * Fetching NAT info is soft-failure: if it rejects, `nat` is left
11
+ * `undefined` on the returned space rather than failing the whole
12
+ * wait.
13
+ */
14
+ includeNat?: boolean;
15
+ /**
16
+ * Delay between polls in milliseconds.
17
+ * Defaults to 5000 (5s)
18
+ */
19
+ intervalMs?: number;
20
+ /**
21
+ * Wall-clock budget in milliseconds before `SpaceNotReadyError`
22
+ * is thrown. Defaults to 1200000 (20 minutes)
23
+ */
24
+ timeoutMs?: number;
25
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { Space, SpaceNat } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { WaitForAllocatedOptions } from './types.js';
4
+ export type SpaceWithNat = Space & {
5
+ /**
6
+ * The space's NAT info, attached when `options.includeNat` is set and
7
+ * the fetch succeeds. Left `undefined` if `includeNat` was not
8
+ * requested, or if the NAT fetch failed (soft-failure).
9
+ */
10
+ nat?: SpaceNat;
11
+ };
12
+ /**
13
+ * Poll `space.info` until the space's `state` is no longer `allocating`,
14
+ * then return it.
15
+ *
16
+ * If the terminal state is not `allocated` (e.g. `deleting`), throws
17
+ * `SpaceNotAllocatedError`. On exceeding `timeoutMs` without reaching a
18
+ * terminal state, throws `SpaceNotReadyError`.
19
+ *
20
+ * Each poll uses the SDK default `Accept: version=3.sdk` with
21
+ * `Accept-Expansion: region` to match the existing CLI behaviour.
22
+ *
23
+ * When `options.includeNat` is true and the space becomes allocated,
24
+ * also fetches `spaceNat.info` and attaches it as `nat` on the result.
25
+ * That fetch is soft-failure: if it rejects, `nat` is left `undefined`
26
+ * rather than failing the wait. A caller abort during the fetch is the
27
+ * exception — it is re-thrown rather than swallowed.
28
+ */
29
+ export declare function waitForAllocated(ctx: Pick<ResourceCtx, 'platform'>, name: string, options?: WaitForAllocatedOptions): Promise<SpaceWithNat>;
@@ -0,0 +1,56 @@
1
+ import { wait } from '../../../utils/wait.js';
2
+ import { SpaceNotAllocatedError, SpaceNotReadyError } from './errors.js';
3
+ const DEFAULT_INTERVAL_MS = 5000;
4
+ const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
5
+ /**
6
+ * Poll `space.info` until the space's `state` is no longer `allocating`,
7
+ * then return it.
8
+ *
9
+ * If the terminal state is not `allocated` (e.g. `deleting`), throws
10
+ * `SpaceNotAllocatedError`. On exceeding `timeoutMs` without reaching a
11
+ * terminal state, throws `SpaceNotReadyError`.
12
+ *
13
+ * Each poll uses the SDK default `Accept: version=3.sdk` with
14
+ * `Accept-Expansion: region` to match the existing CLI behaviour.
15
+ *
16
+ * When `options.includeNat` is true and the space becomes allocated,
17
+ * also fetches `spaceNat.info` and attaches it as `nat` on the result.
18
+ * That fetch is soft-failure: if it rejects, `nat` is left `undefined`
19
+ * rather than failing the wait. A caller abort during the fetch is the
20
+ * exception — it is re-thrown rather than swallowed.
21
+ */
22
+ export async function waitForAllocated(ctx, name, options = {}) {
23
+ const { includeNat, intervalMs = DEFAULT_INTERVAL_MS, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
24
+ signal?.throwIfAborted();
25
+ let platform = ctx.platform.withHeaders({ 'Accept-Expansion': 'region' });
26
+ if (signal) {
27
+ platform = platform.withOptions({ signal });
28
+ }
29
+ const deadline = Date.now() + timeoutMs;
30
+ let space = await platform.space.info(name);
31
+ while (space.state === 'allocating') {
32
+ signal?.throwIfAborted();
33
+ if (Date.now() >= deadline) {
34
+ throw new SpaceNotReadyError(space, timeoutMs);
35
+ }
36
+ await wait(intervalMs, signal);
37
+ space = await platform.space.info(name);
38
+ }
39
+ if (space.state !== 'allocated') {
40
+ throw new SpaceNotAllocatedError(space);
41
+ }
42
+ if (!includeNat) {
43
+ return space;
44
+ }
45
+ try {
46
+ const nat = await platform.spaceNat.info(name);
47
+ return { ...space, nat };
48
+ }
49
+ catch {
50
+ // Soft-failure: a rejected NAT fetch leaves `nat` undefined rather than
51
+ // failing the wait — but a caller abort must not be swallowed here, so
52
+ // re-throw it if the signal aborted during the fetch.
53
+ signal?.throwIfAborted();
54
+ return space;
55
+ }
56
+ }
@@ -0,0 +1,8 @@
1
+ import { type WaitForActiveOptions } from './wait-for-active.js';
2
+ export { VpnConnectionFailedError, VpnConnectionNotReadyError, waitForActive, type WaitForActiveOptions, } from './wait-for-active.js';
3
+ export type VpnConnectionOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare const vpnConnectionExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "vpnConnection", {
7
+ waitForActive: (spaceIdentity: string, vpnConnectionIdentity: string, options?: WaitForActiveOptions) => Promise<import("@heroku/types/3.sdk").VpnConnection>;
8
+ }>;
@@ -0,0 +1,6 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { waitForActive } from './wait-for-active.js';
3
+ export { VpnConnectionFailedError, VpnConnectionNotReadyError, waitForActive, } from './wait-for-active.js';
4
+ export const vpnConnectionExtensions = extendResource('platform', 'vpnConnection', ctx => ({
5
+ waitForActive: (spaceIdentity, vpnConnectionIdentity, options) => waitForActive(ctx, spaceIdentity, vpnConnectionIdentity, options),
6
+ }));
@@ -0,0 +1,39 @@
1
+ import type { VpnConnection } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { Poller } from '../../../utils/poller.js';
4
+ import type { VpnConnectionOptions } from './index.js';
5
+ export declare class VpnConnectionFailedError extends Error {
6
+ readonly id = "vpn_connection_failed";
7
+ constructor(vpnConnection: VpnConnection);
8
+ }
9
+ export declare class VpnConnectionNotReadyError extends Error {
10
+ readonly timeoutMs: number;
11
+ readonly id = "vpn_connection_not_ready";
12
+ constructor(vpnConnection: VpnConnection, timeoutMs: number);
13
+ }
14
+ export type WaitForActiveOptions = VpnConnectionOptions & {
15
+ /**
16
+ * Delay between polls in milliseconds.
17
+ * Defaults to 10000 (10s)
18
+ */
19
+ intervalMs?: number;
20
+ /**
21
+ * Fires `poller.onStart` immediately before polling begins
22
+ * and `poller.onStop` once the connection becomes active.
23
+ */
24
+ poller?: Poller<VpnConnection>;
25
+ /**
26
+ * Wall-clock budget in milliseconds before {@link VpnConnectionNotReadyError}
27
+ * is thrown. Defaults to 1200000 (20 minutes)
28
+ */
29
+ timeoutMs?: number;
30
+ };
31
+ /**
32
+ * Poll `vpnConnection.info` until the connection's `status` is `active`,
33
+ * then return it.
34
+ *
35
+ * If `status` becomes `failed`, throws {@link VpnConnectionFailedError}.
36
+ * On exceeding `timeoutMs` without reaching `active` or `failed`, throws
37
+ * {@link VpnConnectionNotReadyError}.
38
+ */
39
+ export declare function waitForActive(ctx: Pick<ResourceCtx, 'platform'>, spaceIdentity: string, vpnConnectionIdentity: string, options?: WaitForActiveOptions): Promise<VpnConnection>;
@@ -0,0 +1,48 @@
1
+ import { wait } from '../../../utils/wait.js';
2
+ const DEFAULT_INTERVAL_MS = 10 * 1000;
3
+ const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
4
+ export class VpnConnectionFailedError extends Error {
5
+ id = 'vpn_connection_failed';
6
+ constructor(vpnConnection) {
7
+ super(vpnConnection.status_message);
8
+ this.name = 'VpnConnectionFailedError';
9
+ }
10
+ }
11
+ export class VpnConnectionNotReadyError extends Error {
12
+ timeoutMs;
13
+ id = 'vpn_connection_not_ready';
14
+ constructor(vpnConnection, timeoutMs) {
15
+ super(`Timeout waiting for VPN connection ${vpnConnection.name} to become allocated.`);
16
+ this.timeoutMs = timeoutMs;
17
+ this.name = 'VpnConnectionNotReadyError';
18
+ }
19
+ }
20
+ /**
21
+ * Poll `vpnConnection.info` until the connection's `status` is `active`,
22
+ * then return it.
23
+ *
24
+ * If `status` becomes `failed`, throws {@link VpnConnectionFailedError}.
25
+ * On exceeding `timeoutMs` without reaching `active` or `failed`, throws
26
+ * {@link VpnConnectionNotReadyError}.
27
+ */
28
+ export async function waitForActive(ctx, spaceIdentity, vpnConnectionIdentity, options = {}) {
29
+ const { intervalMs = DEFAULT_INTERVAL_MS, poller, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
30
+ signal?.throwIfAborted();
31
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
32
+ const deadline = Date.now() + timeoutMs;
33
+ let vpnConnection = await platform.vpnConnection.info(spaceIdentity, vpnConnectionIdentity);
34
+ poller?.onStart?.(vpnConnection);
35
+ while (vpnConnection.status !== 'active') {
36
+ signal?.throwIfAborted();
37
+ if (vpnConnection.status === 'failed') {
38
+ throw new VpnConnectionFailedError(vpnConnection);
39
+ }
40
+ if (Date.now() >= deadline) {
41
+ throw new VpnConnectionNotReadyError(vpnConnection, timeoutMs);
42
+ }
43
+ await wait(intervalMs, signal);
44
+ vpnConnection = await platform.vpnConnection.info(spaceIdentity, vpnConnectionIdentity);
45
+ }
46
+ poller?.onStop?.(vpnConnection);
47
+ return vpnConnection;
48
+ }
@@ -0,0 +1,5 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { HerokuClient } from '@heroku/types/dashboard-backend';
3
+ import type { RoutesClientExtras } from '../core/create-client.js';
4
+ export type DashboardBackendClient = HerokuClient & RoutesClientExtras<HerokuClient>;
5
+ export declare function createDashboardBackendClient(options?: HerokuApiClientOptions): DashboardBackendClient;
@@ -0,0 +1,10 @@
1
+ import * as routes from '@heroku/types/dashboard-backend/routes';
2
+ import { createClient } from '../core/create-client.js';
3
+ export function createDashboardBackendClient(options = {}) {
4
+ const baseUrl = process.env.HEROKU_PARTICLEBOARD_URL;
5
+ return createClient(routes, {
6
+ service: 'particleboard',
7
+ ...(baseUrl && { baseUrl }),
8
+ ...options,
9
+ });
10
+ }
@@ -0,0 +1,5 @@
1
+ import type { HerokuApiClientOptions } from '@heroku/heroku-fetch';
2
+ import type { HerokuClient } from '@heroku/types/repositories';
3
+ import type { RoutesClientExtras } from '../core/create-client.js';
4
+ export type RepositoriesClient = HerokuClient & RoutesClientExtras<HerokuClient>;
5
+ export declare function createRepositoriesClient(options?: HerokuApiClientOptions): RepositoriesClient;
@@ -0,0 +1,11 @@
1
+ import * as routes from '@heroku/types/repositories/routes';
2
+ import { createClient } from '../core/create-client.js';
3
+ const DEFAULT_REPOSITORIES_BASE_URL = 'https://kolkrabbi.heroku.com';
4
+ export function createRepositoriesClient(options = {}) {
5
+ const baseUrl = process.env.HEROKU_REPOSITORIES_HOST ?? DEFAULT_REPOSITORIES_BASE_URL;
6
+ return createClient(routes, {
7
+ baseUrl,
8
+ service: 'custom',
9
+ ...options,
10
+ });
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroku/sdk",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "Heroku SDK",
5
5
  "author": "Heroku",
6
6
  "type": "module",
@@ -18,10 +18,18 @@
18
18
  "types": "./dist/services/data.d.ts",
19
19
  "default": "./dist/services/data.js"
20
20
  },
21
+ "./dashboard-backend": {
22
+ "types": "./dist/services/dashboard-backend.d.ts",
23
+ "default": "./dist/services/dashboard-backend.js"
24
+ },
21
25
  "./metrics": {
22
26
  "types": "./dist/services/metrics.d.ts",
23
27
  "default": "./dist/services/metrics.js"
24
28
  },
29
+ "./repositories": {
30
+ "types": "./dist/services/repositories.d.ts",
31
+ "default": "./dist/services/repositories.js"
32
+ },
25
33
  "./extensions/platform": {
26
34
  "types": "./dist/resources/extensions/platform.d.ts",
27
35
  "default": "./dist/resources/extensions/platform.js"
@@ -38,6 +46,14 @@
38
46
  "types": "./dist/resources/platform/app/index.d.ts",
39
47
  "default": "./dist/resources/platform/app/index.js"
40
48
  },
49
+ "./resources/platform/container": {
50
+ "types": "./dist/resources/platform/container/index.d.ts",
51
+ "default": "./dist/resources/platform/container/index.js"
52
+ },
53
+ "./resources/platform/domain": {
54
+ "types": "./dist/resources/platform/domain/index.d.ts",
55
+ "default": "./dist/resources/platform/domain/index.js"
56
+ },
41
57
  "./resources/platform/dyno": {
42
58
  "types": "./dist/resources/platform/dyno/index.d.ts",
43
59
  "default": "./dist/resources/platform/dyno/index.js"
@@ -54,10 +70,22 @@
54
70
  "types": "./dist/resources/platform/pipeline/*.d.ts",
55
71
  "default": "./dist/resources/platform/pipeline/*.js"
56
72
  },
73
+ "./resources/platform/sni-endpoint": {
74
+ "types": "./dist/resources/platform/sni-endpoint/index.d.ts",
75
+ "default": "./dist/resources/platform/sni-endpoint/index.js"
76
+ },
77
+ "./resources/platform/telemetry-drain": {
78
+ "types": "./dist/resources/platform/telemetry-drain/index.d.ts",
79
+ "default": "./dist/resources/platform/telemetry-drain/index.js"
80
+ },
57
81
  "./resources/platform/test-run": {
58
82
  "types": "./dist/resources/platform/test-run/index.d.ts",
59
83
  "default": "./dist/resources/platform/test-run/index.js"
60
84
  },
85
+ "./resources/platform/vpn-connection": {
86
+ "types": "./dist/resources/platform/vpn-connection/index.d.ts",
87
+ "default": "./dist/resources/platform/vpn-connection/index.js"
88
+ },
61
89
  "./resources/data/redis": {
62
90
  "types": "./dist/resources/data/redis/index.d.ts",
63
91
  "default": "./dist/resources/data/redis/index.js"
@@ -104,7 +132,7 @@
104
132
  },
105
133
  "dependencies": {
106
134
  "@heroku/heroku-fetch": "^0.1.4",
107
- "@heroku/types": "^4.1.0",
135
+ "@heroku/types": "^4.3.0",
108
136
  "debug": "^4.4.0"
109
137
  }
110
138
  }