@heroku/sdk 0.1.0-alpha.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 (109) hide show
  1. package/LICENSE.txt +207 -0
  2. package/README.md +94 -0
  3. package/dist/compositions/add-on.d.ts +7 -0
  4. package/dist/compositions/add-on.js +6 -0
  5. package/dist/compositions/app.d.ts +8 -0
  6. package/dist/compositions/app.js +11 -0
  7. package/dist/compositions/dyno.d.ts +17 -0
  8. package/dist/compositions/dyno.js +23 -0
  9. package/dist/compositions/pg.d.ts +15 -0
  10. package/dist/compositions/pg.js +56 -0
  11. package/dist/compositions/pipeline.d.ts +33 -0
  12. package/dist/compositions/pipeline.js +146 -0
  13. package/dist/core/create-client.d.ts +32 -0
  14. package/dist/core/create-client.js +48 -0
  15. package/dist/core/dispatcher.d.ts +3 -0
  16. package/dist/core/dispatcher.js +80 -0
  17. package/dist/core/extend-resource.d.ts +24 -0
  18. package/dist/core/extend-resource.js +3 -0
  19. package/dist/core/extensions-proxy.d.ts +2 -0
  20. package/dist/core/extensions-proxy.js +56 -0
  21. package/dist/core/heroku-sdk.d.ts +14 -0
  22. package/dist/core/heroku-sdk.js +54 -0
  23. package/dist/core/interpolate-path.d.ts +3 -0
  24. package/dist/core/interpolate-path.js +13 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +2 -0
  27. package/dist/resources/data/backup.d.ts +9 -0
  28. package/dist/resources/data/backup.js +10 -0
  29. package/dist/resources/data/database.d.ts +54 -0
  30. package/dist/resources/data/database.js +44 -0
  31. package/dist/resources/data/internal/resolve-addon-id.d.ts +2 -0
  32. package/dist/resources/data/internal/resolve-addon-id.js +13 -0
  33. package/dist/resources/data/internal/resolve-pg-database.d.ts +23 -0
  34. package/dist/resources/data/internal/resolve-pg-database.js +52 -0
  35. package/dist/resources/data/maintenance.d.ts +9 -0
  36. package/dist/resources/data/maintenance.js +10 -0
  37. package/dist/resources/data/postgres-credential.d.ts +11 -0
  38. package/dist/resources/data/postgres-credential.js +16 -0
  39. package/dist/resources/data/postgres-database.d.ts +32 -0
  40. package/dist/resources/data/postgres-database.js +14 -0
  41. package/dist/resources/data/postgres.d.ts +9 -0
  42. package/dist/resources/data/postgres.js +10 -0
  43. package/dist/resources/data/transfer-schedule.d.ts +9 -0
  44. package/dist/resources/data/transfer-schedule.js +10 -0
  45. package/dist/resources/data/transfer.d.ts +49 -0
  46. package/dist/resources/data/transfer.js +14 -0
  47. package/dist/resources/extensions/data.d.ts +4 -0
  48. package/dist/resources/extensions/data.js +4 -0
  49. package/dist/resources/extensions/platform.d.ts +8 -0
  50. package/dist/resources/extensions/platform.js +8 -0
  51. package/dist/resources/platform/add-on/create-and-wait.d.ts +28 -0
  52. package/dist/resources/platform/add-on/create-and-wait.js +63 -0
  53. package/dist/resources/platform/add-on/debug.d.ts +2 -0
  54. package/dist/resources/platform/add-on/debug.js +2 -0
  55. package/dist/resources/platform/add-on/describe.d.ts +20 -0
  56. package/dist/resources/platform/add-on/describe.js +51 -0
  57. package/dist/resources/platform/add-on/errors.d.ts +51 -0
  58. package/dist/resources/platform/add-on/errors.js +61 -0
  59. package/dist/resources/platform/add-on/index.d.ts +23 -0
  60. package/dist/resources/platform/add-on/index.js +25 -0
  61. package/dist/resources/platform/add-on/list-plans.d.ts +34 -0
  62. package/dist/resources/platform/add-on/list-plans.js +53 -0
  63. package/dist/resources/platform/add-on/pricing.d.ts +72 -0
  64. package/dist/resources/platform/add-on/pricing.js +100 -0
  65. package/dist/resources/platform/add-on/resolve.d.ts +43 -0
  66. package/dist/resources/platform/add-on/resolve.js +103 -0
  67. package/dist/resources/platform/add-on/types.d.ts +49 -0
  68. package/dist/resources/platform/add-on/types.js +1 -0
  69. package/dist/resources/platform/add-on/upgrade.d.ts +21 -0
  70. package/dist/resources/platform/add-on/upgrade.js +36 -0
  71. package/dist/resources/platform/add-on.d.ts +211 -0
  72. package/dist/resources/platform/add-on.js +354 -0
  73. package/dist/resources/platform/app/index.d.ts +58 -0
  74. package/dist/resources/platform/app/index.js +53 -0
  75. package/dist/resources/platform/app/info.d.ts +30 -0
  76. package/dist/resources/platform/app/info.js +27 -0
  77. package/dist/resources/platform/app.d.ts +56 -0
  78. package/dist/resources/platform/app.js +50 -0
  79. package/dist/resources/platform/dyno/index.d.ts +45 -0
  80. package/dist/resources/platform/dyno/index.js +26 -0
  81. package/dist/resources/platform/dyno.d.ts +45 -0
  82. package/dist/resources/platform/dyno.js +26 -0
  83. package/dist/resources/platform/log-session/index.d.ts +102 -0
  84. package/dist/resources/platform/log-session/index.js +223 -0
  85. package/dist/resources/platform/log-session/parser.d.ts +74 -0
  86. package/dist/resources/platform/log-session/parser.js +71 -0
  87. package/dist/resources/platform/log-session.d.ts +81 -0
  88. package/dist/resources/platform/log-session.js +167 -0
  89. package/dist/resources/platform/pipeline/coupling.d.ts +27 -0
  90. package/dist/resources/platform/pipeline/coupling.js +55 -0
  91. package/dist/resources/platform/pipeline/index.d.ts +53 -0
  92. package/dist/resources/platform/pipeline/index.js +75 -0
  93. package/dist/resources/platform/pipeline/promotion.d.ts +21 -0
  94. package/dist/resources/platform/pipeline/promotion.js +109 -0
  95. package/dist/resources/platform/pipeline-coupling.d.ts +27 -0
  96. package/dist/resources/platform/pipeline-coupling.js +55 -0
  97. package/dist/resources/platform/pipeline-promotion.d.ts +21 -0
  98. package/dist/resources/platform/pipeline-promotion.js +128 -0
  99. package/dist/resources/platform/pipeline.d.ts +53 -0
  100. package/dist/resources/platform/pipeline.js +75 -0
  101. package/dist/resources/platform/shield.d.ts +2 -0
  102. package/dist/resources/platform/shield.js +6 -0
  103. package/dist/services/data.d.ts +5 -0
  104. package/dist/services/data.js +5 -0
  105. package/dist/services/platform.d.ts +5 -0
  106. package/dist/services/platform.js +5 -0
  107. package/dist/utils/wait.d.ts +16 -0
  108. package/dist/utils/wait.js +35 -0
  109. package/package.json +72 -0
@@ -0,0 +1,9 @@
1
+ import type { PostgresInfoResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type PostgresOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function info(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: PostgresOptions): Promise<PostgresInfoResult>;
7
+ export declare const postgresExtensions: import("../../index.js").ResourceExtension<"data", "postgres", {
8
+ info: (appIdentity: string, addonIdentity?: string, options?: PostgresOptions) => Promise<PostgresInfoResult>;
9
+ }>;
@@ -0,0 +1,10 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function info(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.postgres.info(addon.id);
7
+ }
8
+ export const postgresExtensions = extendResource('data', 'postgres', ctx => ({
9
+ info: (appIdentity, addonIdentity, options) => info(ctx, appIdentity, addonIdentity, options),
10
+ }));
@@ -0,0 +1,9 @@
1
+ import type { TransferScheduleListResult } from '@heroku/types/data';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type TransferScheduleOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export declare function list(ctx: Pick<ResourceCtx, 'data' | 'platform'>, appIdentity: string, addonIdentity?: string, options?: TransferScheduleOptions): Promise<TransferScheduleListResult>;
7
+ export declare const transferScheduleExtensions: import("../../index.js").ResourceExtension<"data", "transferSchedule", {
8
+ list: (appIdentity: string, addonIdentity?: string, options?: TransferScheduleOptions) => Promise<TransferScheduleListResult>;
9
+ }>;
@@ -0,0 +1,10 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function list(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.transferSchedule.list(addon.id);
7
+ }
8
+ export const transferScheduleExtensions = extendResource('data', 'transferSchedule', ctx => ({
9
+ list: (appIdentity, addonIdentity, options) => list(ctx, appIdentity, addonIdentity, options),
10
+ }));
@@ -0,0 +1,49 @@
1
+ import type { ResourceCtx } from '../../core/extend-resource.js';
2
+ export type TransferOptions = {
3
+ signal?: AbortSignal;
4
+ };
5
+ export type BackupTransfer = {
6
+ canceled_at: string;
7
+ created_at: string;
8
+ deleted_at: string;
9
+ finished_at: string;
10
+ from_name: string;
11
+ from_type: string;
12
+ from_url: string;
13
+ logs: Array<{
14
+ created_at: string;
15
+ level: string;
16
+ message: string;
17
+ }>;
18
+ num: number;
19
+ num_keep: number;
20
+ options: Record<string, unknown>;
21
+ processed_bytes: number;
22
+ purged_at: string;
23
+ schedule?: {
24
+ uuid: string;
25
+ };
26
+ source_bytes: number;
27
+ started_at: string;
28
+ succeeded: boolean;
29
+ to_name: string;
30
+ to_type: string;
31
+ to_url: string;
32
+ updated_at: string;
33
+ uuid: string;
34
+ warnings: number;
35
+ };
36
+ type TransferDataClient = {
37
+ transfer: {
38
+ listByApp(name: string): Promise<BackupTransfer[]>;
39
+ };
40
+ };
41
+ type TransferCtx = {
42
+ data: TransferDataClient;
43
+ platform: ResourceCtx['platform'];
44
+ };
45
+ export declare function listByApp(ctx: TransferCtx, appIdentity: string, addonIdentity?: string, options?: TransferOptions): Promise<BackupTransfer[]>;
46
+ export declare const transferExtensions: import("../../core/extend-resource.js").ResourceExtension<"data", "transfer", {
47
+ listByApp: (appIdentity: string, addonIdentity?: string, options?: TransferOptions) => Promise<BackupTransfer[]>;
48
+ }>;
49
+ export {};
@@ -0,0 +1,14 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ import { resolvePgDatabase } from './internal/resolve-pg-database.js';
3
+ export async function listByApp(ctx, appIdentity, addonIdentity, options = {}) {
4
+ options.signal?.throwIfAborted();
5
+ const addon = await resolvePgDatabase(ctx, { appIdentity, input: addonIdentity, ...options });
6
+ return ctx.data.transfer.listByApp(addon.id);
7
+ }
8
+ export const transferExtensions = extendResource('data', 'transfer', rawCtx => {
9
+ // Bridge heroku-types' loose client types to the adapter's strongly-typed contract
10
+ const ctx = rawCtx;
11
+ return {
12
+ listByApp: (appIdentity, addonIdentity, options) => listByApp(ctx, appIdentity, addonIdentity, options),
13
+ };
14
+ });
@@ -0,0 +1,4 @@
1
+ export { databaseExtensions } from '../data/database.js';
2
+ export { maintenanceExtensions } from '../data/maintenance.js';
3
+ export { postgresDatabaseExtensions } from '../data/postgres-database.js';
4
+ export { transferExtensions } from '../data/transfer.js';
@@ -0,0 +1,4 @@
1
+ export { databaseExtensions } from '../data/database.js';
2
+ export { maintenanceExtensions } from '../data/maintenance.js';
3
+ export { postgresDatabaseExtensions } from '../data/postgres-database.js';
4
+ export { transferExtensions } from '../data/transfer.js';
@@ -0,0 +1,8 @@
1
+ export { addOnExtensions } from '../platform/add-on/index.js';
2
+ export { appExtensions, type AppInfo, type PipelineCouplingDetail } from '../platform/app/index.js';
3
+ export { dynoExtensions } from '../platform/dyno/index.js';
4
+ export { type AttachmentAttachedEvent, type AttachmentDetachedEvent, type AttachmentUpdatedEvent, type HerokuLogEvent, logSessionExtensions, parseHerokuLogLine, type ProvisioningCompletedEvent, type ScaledToEntry, type ScaledToEvent, type StartingProcessEvent, type StateChangedEvent, } from '../platform/log-session/index.js';
5
+ export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
6
+ export { pipelineExtensions } from '../platform/pipeline/index.js';
7
+ export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
8
+ export { privateToShield, shieldToPrivate } from '../platform/shield.js';
@@ -0,0 +1,8 @@
1
+ export { addOnExtensions } from '../platform/add-on/index.js';
2
+ export { appExtensions } from '../platform/app/index.js';
3
+ export { dynoExtensions } from '../platform/dyno/index.js';
4
+ export { logSessionExtensions, parseHerokuLogLine, } from '../platform/log-session/index.js';
5
+ export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
6
+ export { pipelineExtensions } from '../platform/pipeline/index.js';
7
+ export { pipelinePromotionExtensions } from '../platform/pipeline/promotion.js';
8
+ export { privateToShield, shieldToPrivate } from '../platform/shield.js';
@@ -0,0 +1,28 @@
1
+ import type { AddOn, AddOnCreateOpts } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { CreateAndWaitOptions } from './types.js';
4
+ /**
5
+ * Create an add-on and optionally wait for provisioning to complete.
6
+ *
7
+ * Wraps `addOn.create` with three pieces of orchestration:
8
+ *
9
+ * - 423 `confirmation_required` from the platform is converted to a
10
+ * typed `AddonConfirmationRequiredError`. Callers should catch
11
+ * this, prompt the user for confirmation, and retry the call with
12
+ * `body.confirm` set.
13
+ * - When `wait: true` is set, polls `addOn.infoByApp` on a
14
+ * `waitIntervalMs` cadence until the add-on's `state` is no longer
15
+ * `provisioning`. Throws `AddonProvisioningFailedError` if the
16
+ * terminal state is `deprovisioned`.
17
+ * - `options.onProvisioning` fires once after the create response
18
+ * when polling is about to begin, letting callers surface a
19
+ * two-phase status display ("Created" → "Waiting").
20
+ *
21
+ * Both the create and poll calls are issued with `Accept-Expansion:
22
+ * addon_service,plan` so the response includes the full plan shape
23
+ * (price, unit, etc.). The create call additionally sets
24
+ * `X-Heroku-Legacy-Provider-Messages: true` to opt into
25
+ * provider-supplied `provision_message` text on the response — without
26
+ * it the platform silently omits that field.
27
+ */
28
+ export declare function createAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions): Promise<AddOn>;
@@ -0,0 +1,63 @@
1
+ import { HerokuApiError } from '@heroku/heroku-fetch';
2
+ import { wait } from '../../../utils/wait.js';
3
+ import { AddonConfirmationRequiredError, AddonProvisioningFailedError } from './errors.js';
4
+ const DEFAULT_CREATE_WAIT_INTERVAL_MS = 5000;
5
+ /**
6
+ * Create an add-on and optionally wait for provisioning to complete.
7
+ *
8
+ * Wraps `addOn.create` with three pieces of orchestration:
9
+ *
10
+ * - 423 `confirmation_required` from the platform is converted to a
11
+ * typed `AddonConfirmationRequiredError`. Callers should catch
12
+ * this, prompt the user for confirmation, and retry the call with
13
+ * `body.confirm` set.
14
+ * - When `wait: true` is set, polls `addOn.infoByApp` on a
15
+ * `waitIntervalMs` cadence until the add-on's `state` is no longer
16
+ * `provisioning`. Throws `AddonProvisioningFailedError` if the
17
+ * terminal state is `deprovisioned`.
18
+ * - `options.onProvisioning` fires once after the create response
19
+ * when polling is about to begin, letting callers surface a
20
+ * two-phase status display ("Created" → "Waiting").
21
+ *
22
+ * Both the create and poll calls are issued with `Accept-Expansion:
23
+ * addon_service,plan` so the response includes the full plan shape
24
+ * (price, unit, etc.). The create call additionally sets
25
+ * `X-Heroku-Legacy-Provider-Messages: true` to opt into
26
+ * provider-supplied `provision_message` text on the response — without
27
+ * it the platform silently omits that field.
28
+ */
29
+ export async function createAndWait(ctx, appIdentity, body, options = {}) {
30
+ options.signal?.throwIfAborted();
31
+ const platform = ctx.platform.withHeaders({ 'Accept-Expansion': 'addon_service,plan' });
32
+ let addon;
33
+ try {
34
+ addon = await platform
35
+ .withHeaders({ 'X-Heroku-Legacy-Provider-Messages': 'true' })
36
+ .addOn.create(appIdentity, body);
37
+ }
38
+ catch (error) {
39
+ if (error instanceof HerokuApiError && error.id === 'confirmation_required') {
40
+ throw new AddonConfirmationRequiredError(error.message);
41
+ }
42
+ throw error;
43
+ }
44
+ if (!options.wait || addon.state !== 'provisioning') {
45
+ if (addon.state === 'deprovisioned') {
46
+ throw new AddonProvisioningFailedError(addon);
47
+ }
48
+ return addon;
49
+ }
50
+ await options.onProvisioning?.(addon);
51
+ const intervalMs = options.waitIntervalMs ?? DEFAULT_CREATE_WAIT_INTERVAL_MS;
52
+ while (addon.state === 'provisioning') {
53
+ options.signal?.throwIfAborted();
54
+ // eslint-disable-next-line no-await-in-loop
55
+ await wait(intervalMs, options.signal);
56
+ // eslint-disable-next-line no-await-in-loop
57
+ addon = await platform.addOn.infoByApp(appIdentity, addon.name);
58
+ }
59
+ if (addon.state === 'deprovisioned') {
60
+ throw new AddonProvisioningFailedError(addon);
61
+ }
62
+ return addon;
63
+ }
@@ -0,0 +1,2 @@
1
+ import createDebug from 'debug';
2
+ export declare const debug: createDebug.Debugger;
@@ -0,0 +1,2 @@
1
+ import createDebug from 'debug';
2
+ export const debug = createDebug('heroku:sdk:resources:add-on');
@@ -0,0 +1,20 @@
1
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
2
+ import type { DescribedAddOn, ResolveAddonOptions } from './types.js';
3
+ /**
4
+ * Resolve, describe, and decorate an add-on for display.
5
+ *
6
+ * - Resolves the add-on by identity (optionally scoped to an app).
7
+ * - Loads the add-on's attachments.
8
+ * - Returns a copy with `plan.price` set from `billed_price` (so the
9
+ * value reflects any grandfathered/contract pricing).
10
+ *
11
+ * If `appIdentity` is provided and the platform returns 404 (resource
12
+ * `add_on`), the resolve falls back to a global lookup. This handles
13
+ * the case where the add-on belongs to a different app than the one
14
+ * supplied.
15
+ *
16
+ * Requests the `version=3.sdk` accept variant with `addon_service,plan`
17
+ * expansion so the resolved add-on includes the full `Plan` shape
18
+ * (`price.unit`, etc.) needed to render pricing.
19
+ */
20
+ export declare function describeAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ResolveAddonOptions): Promise<DescribedAddOn>;
@@ -0,0 +1,51 @@
1
+ import { resolveAddonInternal } from './resolve.js';
2
+ /**
3
+ * Resolve, describe, and decorate an add-on for display.
4
+ *
5
+ * - Resolves the add-on by identity (optionally scoped to an app).
6
+ * - Loads the add-on's attachments.
7
+ * - Returns a copy with `plan.price` set from `billed_price` (so the
8
+ * value reflects any grandfathered/contract pricing).
9
+ *
10
+ * If `appIdentity` is provided and the platform returns 404 (resource
11
+ * `add_on`), the resolve falls back to a global lookup. This handles
12
+ * the case where the add-on belongs to a different app than the one
13
+ * supplied.
14
+ *
15
+ * Requests the `version=3.sdk` accept variant with `addon_service,plan`
16
+ * expansion so the resolved add-on includes the full `Plan` shape
17
+ * (`price.unit`, etc.) needed to render pricing.
18
+ */
19
+ export async function describeAddon(ctx, addonIdentity, options = {}) {
20
+ options.signal?.throwIfAborted();
21
+ // Accept-Expansion is scoped to the resolve call (addOn.resolution
22
+ // serializer accepts addon_service + plan expansions). The
23
+ // /addons/:id/addon-attachments endpoint uses Accept-Inclusion, not
24
+ // Accept-Expansion, and rejects an unknown expansion header.
25
+ const resolvingPlatform = ctx.platform.withHeaders({
26
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
27
+ 'Accept-Expansion': 'addon_service,plan',
28
+ });
29
+ const addon = await resolveAddonInternal(resolvingPlatform, addonIdentity, {
30
+ addonService: options.addonService,
31
+ appIdentity: options.appIdentity,
32
+ });
33
+ options.signal?.throwIfAborted();
34
+ const attachments = await ctx.platform.addOnAttachment.listByAddOn(addon.id);
35
+ const plan = addon.plan;
36
+ return {
37
+ ...addon,
38
+ ...(plan && { plan: { ...plan, price: grandfatheredPrice(addon) } }),
39
+ attachments,
40
+ };
41
+ }
42
+ function grandfatheredPrice(addon) {
43
+ const price = addon.plan?.price;
44
+ return {
45
+ ...price,
46
+ ...(addon.billed_price && {
47
+ cents: addon.billed_price.cents,
48
+ contract: addon.billed_price.contract,
49
+ }),
50
+ };
51
+ }
@@ -0,0 +1,51 @@
1
+ import type { AddOn } from '@heroku/types/3.sdk';
2
+ export declare class AddonNotFoundError extends Error {
3
+ readonly resource: string;
4
+ readonly id = "not_found";
5
+ readonly statusCode = 404;
6
+ constructor(resource?: string);
7
+ get body(): {
8
+ id: string;
9
+ message: string;
10
+ resource: string;
11
+ };
12
+ }
13
+ export declare class AddonAmbiguousError extends Error {
14
+ readonly matches: AddOn[];
15
+ readonly id = "multiple_matches";
16
+ readonly statusCode = 422;
17
+ constructor(matches: AddOn[]);
18
+ get body(): {
19
+ id: string;
20
+ message: string;
21
+ };
22
+ }
23
+ /**
24
+ * Thrown by `createAndWait` when the Platform returns 423
25
+ * `confirmation_required`. Callers that want to prompt the user should
26
+ * catch this, gather a confirmation value, and retry the call with
27
+ * `body.confirm` set.
28
+ */
29
+ export declare class AddonConfirmationRequiredError extends Error {
30
+ readonly platformMessage: string;
31
+ readonly id = "confirmation_required";
32
+ readonly statusCode = 423;
33
+ constructor(platformMessage: string);
34
+ get body(): {
35
+ id: string;
36
+ message: string;
37
+ };
38
+ }
39
+ /**
40
+ * Thrown by `createAndWait` when the Platform reports the add-on
41
+ * settled into a non-provisioned terminal state (e.g. `deprovisioned`).
42
+ */
43
+ export declare class AddonProvisioningFailedError extends Error {
44
+ readonly addon: AddOn;
45
+ readonly id = "provisioning_failed";
46
+ constructor(addon: AddOn);
47
+ get body(): {
48
+ id: string;
49
+ message: string;
50
+ };
51
+ }
@@ -0,0 +1,61 @@
1
+ export class AddonNotFoundError extends Error {
2
+ resource;
3
+ id = 'not_found';
4
+ statusCode = 404;
5
+ constructor(resource = 'addon') {
6
+ super(`Couldn't find that ${resource}.`);
7
+ this.resource = resource;
8
+ this.name = 'AddonNotFoundError';
9
+ }
10
+ get body() {
11
+ return { id: this.id, message: this.message, resource: this.resource };
12
+ }
13
+ }
14
+ export class AddonAmbiguousError extends Error {
15
+ matches;
16
+ id = 'multiple_matches';
17
+ statusCode = 422;
18
+ constructor(matches) {
19
+ super(`Ambiguous identifier; multiple matching add-ons found: ${matches.map(m => m.name).join(', ')}.`);
20
+ this.matches = matches;
21
+ this.name = 'AddonAmbiguousError';
22
+ }
23
+ get body() {
24
+ return { id: this.id, message: this.message };
25
+ }
26
+ }
27
+ /**
28
+ * Thrown by `createAndWait` when the Platform returns 423
29
+ * `confirmation_required`. Callers that want to prompt the user should
30
+ * catch this, gather a confirmation value, and retry the call with
31
+ * `body.confirm` set.
32
+ */
33
+ export class AddonConfirmationRequiredError extends Error {
34
+ platformMessage;
35
+ id = 'confirmation_required';
36
+ statusCode = 423;
37
+ constructor(platformMessage) {
38
+ super(platformMessage);
39
+ this.platformMessage = platformMessage;
40
+ this.name = 'AddonConfirmationRequiredError';
41
+ }
42
+ get body() {
43
+ return { id: this.id, message: this.message };
44
+ }
45
+ }
46
+ /**
47
+ * Thrown by `createAndWait` when the Platform reports the add-on
48
+ * settled into a non-provisioned terminal state (e.g. `deprovisioned`).
49
+ */
50
+ export class AddonProvisioningFailedError extends Error {
51
+ addon;
52
+ id = 'provisioning_failed';
53
+ constructor(addon) {
54
+ super(`The add-on was unable to be created, with status ${addon.state}.`);
55
+ this.addon = addon;
56
+ this.name = 'AddonProvisioningFailedError';
57
+ }
58
+ get body() {
59
+ return { id: this.id, message: this.message };
60
+ }
61
+ }
@@ -0,0 +1,23 @@
1
+ import type { AddOnCreateOpts } from '@heroku/types/3.sdk';
2
+ import type { ListPlansForAddonOptions } from './list-plans.js';
3
+ import type { AddOnOptions, CreateAndWaitOptions, ResolveAddonOptions, UpgradeAddOnOptions } from './types.js';
4
+ import { formatPlanPriceLabel, priceForPlan } from './pricing.js';
5
+ export { createAndWait } from './create-and-wait.js';
6
+ export { describeAddon } from './describe.js';
7
+ export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
8
+ export { listPlans, listPlansForAddon, type ListPlansForAddonOptions } from './list-plans.js';
9
+ export { formatPlanPriceLabel, type FormatPlanPriceLabelOptions, type PlanPriceBreakdown, priceForPlan, } from './pricing.js';
10
+ export { resolveAddon, resolveAddonByAttachment } from './resolve.js';
11
+ export type { AddOnOptions, CreateAndWaitOptions, DescribedAddOn, ResolveAddonOptions, ResolvedAddOn, UpgradeAddOnOptions, } from './types.js';
12
+ export { upgrade } from './upgrade.js';
13
+ export declare const addOnExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "addOn", {
14
+ createAndWait: (appIdentity: string, body: AddOnCreateOpts, options?: CreateAndWaitOptions) => Promise<import("@heroku/types/3.sdk").AddOn>;
15
+ describe: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").DescribedAddOn>;
16
+ formatPlanPriceLabel: typeof formatPlanPriceLabel;
17
+ listPlans: (serviceIdentity: string, options?: AddOnOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
18
+ listPlansForAddon: (addonIdentity: string, options?: ListPlansForAddonOptions) => Promise<import("@heroku/types/3.sdk").Plan[]>;
19
+ priceForPlan: typeof priceForPlan;
20
+ resolve: (addonIdentity: string, options?: ResolveAddonOptions) => Promise<import("./types.js").ResolvedAddOn>;
21
+ resolveByAttachment: (appIdentity: string, attachmentName: string, options?: AddOnOptions) => Promise<import("./types.js").ResolvedAddOn>;
22
+ upgrade: (addonIdentity: string, plan: string, options?: UpgradeAddOnOptions) => Promise<import("@heroku/types/3.sdk").AddOn>;
23
+ }>;
@@ -0,0 +1,25 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { createAndWait } from './create-and-wait.js';
3
+ import { describeAddon } from './describe.js';
4
+ import { listPlans, listPlansForAddon } from './list-plans.js';
5
+ import { formatPlanPriceLabel, priceForPlan } from './pricing.js';
6
+ import { resolveAddon, resolveAddonByAttachment } from './resolve.js';
7
+ import { upgrade } from './upgrade.js';
8
+ export { createAndWait } from './create-and-wait.js';
9
+ export { describeAddon } from './describe.js';
10
+ export { AddonAmbiguousError, AddonConfirmationRequiredError, AddonNotFoundError, AddonProvisioningFailedError, } from './errors.js';
11
+ export { listPlans, listPlansForAddon } from './list-plans.js';
12
+ export { formatPlanPriceLabel, priceForPlan, } from './pricing.js';
13
+ export { resolveAddon, resolveAddonByAttachment } from './resolve.js';
14
+ export { upgrade } from './upgrade.js';
15
+ export const addOnExtensions = extendResource('platform', 'addOn', ctx => ({
16
+ createAndWait: (appIdentity, body, options) => createAndWait(ctx, appIdentity, body, options),
17
+ describe: (addonIdentity, options) => describeAddon(ctx, addonIdentity, options),
18
+ formatPlanPriceLabel,
19
+ listPlans: (serviceIdentity, options) => listPlans(ctx, serviceIdentity, options),
20
+ listPlansForAddon: (addonIdentity, options) => listPlansForAddon(ctx, addonIdentity, options),
21
+ priceForPlan,
22
+ resolve: (addonIdentity, options) => resolveAddon(ctx, addonIdentity, options),
23
+ resolveByAttachment: (appIdentity, attachmentName, options) => resolveAddonByAttachment(ctx, appIdentity, attachmentName, options),
24
+ upgrade: (addonIdentity, plan, options) => upgrade(ctx, addonIdentity, plan, options),
25
+ }));
@@ -0,0 +1,34 @@
1
+ import type { Plan } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { AddOnOptions, ResolveAddonOptions } from './types.js';
4
+ /**
5
+ * Subset of {@link ResolveAddonOptions} accepted by
6
+ * {@link listPlansForAddon}. The `addonService` filter is intentionally
7
+ * omitted: once you've pinned an add-on identity, filtering its
8
+ * resolution by service name is nonsensical.
9
+ */
10
+ export type ListPlansForAddonOptions = Pick<ResolveAddonOptions, 'appIdentity' | 'signal'>;
11
+ /**
12
+ * List the plans available for an add-on service, sorted ascending by
13
+ * `price.cents`. Plans without a price (or with equal prices) are
14
+ * returned later in the list.
15
+ */
16
+ export declare function listPlans(ctx: Pick<ResourceCtx, 'platform'>, serviceIdentity: string, options?: AddOnOptions): Promise<Plan[]>;
17
+ /**
18
+ * List the plans available for the add-on service of an existing
19
+ * add-on, sorted ascending by `price.cents`.
20
+ *
21
+ * Convenience wrapper that resolves the add-on first to look up its
22
+ * `addon_service.name`, then delegates to `listPlans`. Saves callers
23
+ * the dance of fetching the add-on, plucking its service id/name, and
24
+ * passing it through.
25
+ *
26
+ * `addonIdentity` accepts the same forms as `resolveAddon`:
27
+ * - a UUID
28
+ * - a globally-unique add-on name
29
+ * - a namespaced credential reference (`name::SECONDARY`)
30
+ *
31
+ * Pass `appIdentity` via `options` when the add-on identity is
32
+ * scoped to an app (same semantics as `resolveAddon`).
33
+ */
34
+ export declare function listPlansForAddon(ctx: Pick<ResourceCtx, 'platform'>, addonIdentity: string, options?: ListPlansForAddonOptions): Promise<Plan[]>;
@@ -0,0 +1,53 @@
1
+ import { AddonNotFoundError } from './errors.js';
2
+ import { resolveAddonInternal } from './resolve.js';
3
+ /**
4
+ * List the plans available for an add-on service, sorted ascending by
5
+ * `price.cents`. Plans without a price (or with equal prices) are
6
+ * returned later in the list.
7
+ */
8
+ export async function listPlans(ctx, serviceIdentity, options = {}) {
9
+ options.signal?.throwIfAborted();
10
+ const plans = await ctx.platform.plan.listByAddOn(serviceIdentity);
11
+ return [...plans].sort(byPriceCentsAsc);
12
+ }
13
+ /**
14
+ * List the plans available for the add-on service of an existing
15
+ * add-on, sorted ascending by `price.cents`.
16
+ *
17
+ * Convenience wrapper that resolves the add-on first to look up its
18
+ * `addon_service.name`, then delegates to `listPlans`. Saves callers
19
+ * the dance of fetching the add-on, plucking its service id/name, and
20
+ * passing it through.
21
+ *
22
+ * `addonIdentity` accepts the same forms as `resolveAddon`:
23
+ * - a UUID
24
+ * - a globally-unique add-on name
25
+ * - a namespaced credential reference (`name::SECONDARY`)
26
+ *
27
+ * Pass `appIdentity` via `options` when the add-on identity is
28
+ * scoped to an app (same semantics as `resolveAddon`).
29
+ */
30
+ export async function listPlansForAddon(ctx, addonIdentity, options = {}) {
31
+ options.signal?.throwIfAborted();
32
+ const addon = await resolveAddonInternal(ctx.platform, addonIdentity, {
33
+ appIdentity: options.appIdentity,
34
+ });
35
+ // The route accepts either name or id; name is stable across
36
+ // environments and easier to identify in logs.
37
+ const serviceName = addon.addon_service?.name;
38
+ if (!serviceName) {
39
+ // The resolve found the add-on but the response was missing the
40
+ // service info we need to look up plans. Treat this as not-found
41
+ // for the caller's purpose (they wanted a plans list and we
42
+ // can't produce one).
43
+ throw new AddonNotFoundError();
44
+ }
45
+ return listPlans(ctx, serviceName, options);
46
+ }
47
+ function byPriceCentsAsc(a, b) {
48
+ return sortableCents(a) - sortableCents(b);
49
+ }
50
+ function sortableCents(plan) {
51
+ const cents = plan.price?.cents;
52
+ return typeof cents === 'number' ? cents : Number.MAX_SAFE_INTEGER;
53
+ }
@@ -0,0 +1,72 @@
1
+ import type { Plan } from '@heroku/types/3.sdk';
2
+ export type PlanPriceBreakdown = {
3
+ /** raw price as reported by the API, in cents */
4
+ cents: number;
5
+ /** True when the price is negotiated outside monthly add-on billing */
6
+ contract: boolean;
7
+ /** True when the price is billed per use rather than at a fixed cadence */
8
+ metered: boolean;
9
+ /**
10
+ * Equivalent hourly cost in cents (may be fractional). Equal to
11
+ * `cents` when `unit` is `'hour'`; computed as `cents /
12
+ * HOURS_PER_MONTH` when `unit` is `'month'`. `undefined` when the
13
+ * unit is anything else — callers can omit the per-hour label
14
+ * rather than render misleading numbers.
15
+ */
16
+ perHourCents: number | undefined;
17
+ /**
18
+ * Equivalent monthly cost in cents. Equal to `cents` when `unit` is
19
+ * `'month'`; computed as `cents * HOURS_PER_MONTH` when `unit` is
20
+ * `'hour'`. `undefined` when the unit is anything else.
21
+ */
22
+ perMonthCents: number | undefined;
23
+ /** unit reported by the API (e.g. `'month'`, `'hour'`) */
24
+ unit: string;
25
+ };
26
+ /**
27
+ * Break a plan's price down into monthly- and hourly-equivalent
28
+ * cents, normalizing across the API's billing unit. Consumers that
29
+ * display "max $X/month (~$Y/hour)" labels should call this rather
30
+ * than dividing `cents` by hours themselves; centralizing the math
31
+ * keeps the 30-day-month assumption (Heroku's documented convention)
32
+ * in one place.
33
+ *
34
+ * Returns `undefined` if the plan has no usable `price.cents` value.
35
+ * `Plan.price` is typed as optional in `@heroku/types`, and consumers
36
+ * occasionally receive partial responses depending on the Accept
37
+ * variant.
38
+ *
39
+ * `perMonthCents` and `perHourCents` are themselves `undefined` when
40
+ * the API reports a unit other than `'month'` or `'hour'` — callers
41
+ * should omit those parts of their label rather than show fabricated
42
+ * numbers.
43
+ *
44
+ * @param plan The plan to break down. Pass the whole `Plan` rather
45
+ * than just `plan.price` so the helper can pick up future fields
46
+ * without API churn at call sites.
47
+ */
48
+ export declare function priceForPlan(plan: Plan): PlanPriceBreakdown | undefined;
49
+ export type FormatPlanPriceLabelOptions = {
50
+ /** ISO 4217 currency code passed to `Intl.NumberFormat`. Default: `'USD'`. */
51
+ currency?: string;
52
+ /** BCP 47 locale tag passed to `Intl.NumberFormat`. Default: `'en-US'`. */
53
+ locale?: string;
54
+ };
55
+ /**
56
+ * Format a plan's price as a human-readable suffix:
57
+ * `"$0.007 / hour (Max $5/month)"`. Returns an empty string when the
58
+ * plan has no displayable price (no `price.cents`, an unknown unit,
59
+ * a metered plan, or a contract-priced plan) — callers can compose
60
+ * their final label as `${name}${suffix ? ' - ' + suffix : ''}`
61
+ * without per-call defensive checks.
62
+ *
63
+ * Locale and currency default to `'en-US'` / `'USD'`. Pass options
64
+ * to override.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const suffix = formatPlanPriceLabel(plan);
69
+ * const label = suffix ? `${plan.human_name} - ${suffix}` : plan.human_name;
70
+ * ```
71
+ */
72
+ export declare function formatPlanPriceLabel(plan: Plan, options?: FormatPlanPriceLabelOptions): string;