@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,354 @@
1
+ import { HerokuApiError, NotFoundError } from '@heroku/heroku-fetch';
2
+ import createDebug from 'debug';
3
+ import { extendResource } from '../../core/extend-resource.js';
4
+ const debug = createDebug('heroku:sdk:resources:add-on');
5
+ export class AddonNotFoundError extends Error {
6
+ resource;
7
+ id = 'not_found';
8
+ statusCode = 404;
9
+ constructor(resource = 'addon') {
10
+ super(`Couldn't find that ${resource}.`);
11
+ this.resource = resource;
12
+ this.name = 'AddonNotFoundError';
13
+ }
14
+ get body() {
15
+ return { id: this.id, message: this.message, resource: this.resource };
16
+ }
17
+ }
18
+ export class AddonAmbiguousError extends Error {
19
+ matches;
20
+ id = 'multiple_matches';
21
+ statusCode = 422;
22
+ constructor(matches) {
23
+ super(`Ambiguous identifier; multiple matching add-ons found: ${matches.map(m => m.name).join(', ')}.`);
24
+ this.matches = matches;
25
+ this.name = 'AddonAmbiguousError';
26
+ }
27
+ get body() {
28
+ return { id: this.id, message: this.message };
29
+ }
30
+ }
31
+ /**
32
+ * Thrown by `createAndWait` when the Platform returns 423
33
+ * `confirmation_required`. Callers that want to prompt the user should
34
+ * catch this, gather a confirmation value, and retry the call with
35
+ * `body.confirm` set.
36
+ */
37
+ export class AddonConfirmationRequiredError extends Error {
38
+ platformMessage;
39
+ id = 'confirmation_required';
40
+ statusCode = 423;
41
+ constructor(platformMessage) {
42
+ super(platformMessage);
43
+ this.platformMessage = platformMessage;
44
+ this.name = 'AddonConfirmationRequiredError';
45
+ }
46
+ get body() {
47
+ return { id: this.id, message: this.message };
48
+ }
49
+ }
50
+ /**
51
+ * Thrown by `createAndWait` when the Platform reports the add-on
52
+ * settled into a non-provisioned terminal state (e.g. `deprovisioned`).
53
+ */
54
+ export class AddonProvisioningFailedError extends Error {
55
+ addon;
56
+ id = 'provisioning_failed';
57
+ constructor(addon) {
58
+ super(`The add-on was unable to be created, with status ${addon.state}.`);
59
+ this.addon = addon;
60
+ this.name = 'AddonProvisioningFailedError';
61
+ }
62
+ get body() {
63
+ return { id: this.id, message: this.message };
64
+ }
65
+ }
66
+ /**
67
+ * Change the plan of an add-on.
68
+ *
69
+ * Resolves the add-on first so the caller can pass any identifier
70
+ * (UUID, globally unique name, or namespaced `service::name`). When
71
+ * `appIdentity` is provided, the resolve is scoped to that app and
72
+ * falls back to a global resolve if the platform returns 404 add_on.
73
+ *
74
+ * If `plan` is unqualified (no `:`), it's prefixed with the resolved
75
+ * add-on's `addon_service.name` — so callers can pass `hobby` rather
76
+ * than `heroku-redis:hobby` if they don't already know the service.
77
+ *
78
+ * `options.onResolved` fires after the resolve and before the update,
79
+ * receiving the resolved add-on. Useful for surfacing pre-update
80
+ * state (the add-on's current plan, app, etc.) without an extra
81
+ * round-trip.
82
+ */
83
+ export async function upgrade(ctx, addonIdentity, plan, options = {}) {
84
+ options.signal?.throwIfAborted();
85
+ const addon = await resolveAddonInternal(ctx.platform, addonIdentity, {
86
+ addonService: options.addonService,
87
+ appIdentity: options.appIdentity,
88
+ });
89
+ await options.onResolved?.(addon);
90
+ options.signal?.throwIfAborted();
91
+ const qualifiedPlan = plan.includes(':')
92
+ ? plan
93
+ : `${addon.addon_service?.name}:${plan}`;
94
+ if (qualifiedPlan !== plan) {
95
+ debug('upgrade plan qualified plan=%s qualified=%s', plan, qualifiedPlan);
96
+ }
97
+ debug('upgrade addon=%s app=%s plan=%s', addon.id, addon.app.id, qualifiedPlan);
98
+ return ctx.platform.addOn.update(addon.app.id, addon.id, { plan: qualifiedPlan });
99
+ }
100
+ const DEFAULT_CREATE_WAIT_INTERVAL_MS = 5000;
101
+ /**
102
+ * Create an add-on and optionally wait for provisioning to complete.
103
+ *
104
+ * Wraps `addOn.create` with three pieces of orchestration:
105
+ *
106
+ * - 423 `confirmation_required` from the platform is converted to a
107
+ * typed `AddonConfirmationRequiredError`. Callers should catch
108
+ * this, prompt the user for confirmation, and retry the call with
109
+ * `body.confirm` set.
110
+ * - When `wait: true` is set, polls `addOn.infoByApp` on a
111
+ * `waitIntervalMs` cadence until the add-on's `state` is no longer
112
+ * `provisioning`. Throws `AddonProvisioningFailedError` if the
113
+ * terminal state is `deprovisioned`.
114
+ * - `options.onProvisioning` fires once after the create response
115
+ * when polling is about to begin, letting callers surface a
116
+ * two-phase status display ("Created" → "Waiting").
117
+ *
118
+ * Both the create and poll calls are issued with `Accept-Expansion:
119
+ * addon_service,plan` so the response includes the full plan shape
120
+ * (price, unit, etc.). The create call additionally sets
121
+ * `X-Heroku-Legacy-Provider-Messages: true` to opt into
122
+ * provider-supplied `provision_message` text on the response — without
123
+ * it the platform silently omits that field.
124
+ */
125
+ export async function createAndWait(ctx, appIdentity, body, options = {}) {
126
+ options.signal?.throwIfAborted();
127
+ const platform = ctx.platform.withHeaders({ 'Accept-Expansion': 'addon_service,plan' });
128
+ let addon;
129
+ try {
130
+ addon = await platform
131
+ .withHeaders({ 'X-Heroku-Legacy-Provider-Messages': 'true' })
132
+ .addOn.create(appIdentity, body);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof HerokuApiError && error.id === 'confirmation_required') {
136
+ throw new AddonConfirmationRequiredError(error.message);
137
+ }
138
+ throw error;
139
+ }
140
+ if (!options.wait || addon.state !== 'provisioning') {
141
+ if (addon.state === 'deprovisioned') {
142
+ throw new AddonProvisioningFailedError(addon);
143
+ }
144
+ return addon;
145
+ }
146
+ await options.onProvisioning?.(addon);
147
+ const intervalMs = options.waitIntervalMs ?? DEFAULT_CREATE_WAIT_INTERVAL_MS;
148
+ while (addon.state === 'provisioning') {
149
+ options.signal?.throwIfAborted();
150
+ // eslint-disable-next-line no-await-in-loop
151
+ await wait(intervalMs, options.signal);
152
+ // eslint-disable-next-line no-await-in-loop
153
+ addon = await platform.addOn.infoByApp(appIdentity, addon.name);
154
+ }
155
+ if (addon.state === 'deprovisioned') {
156
+ throw new AddonProvisioningFailedError(addon);
157
+ }
158
+ return addon;
159
+ }
160
+ function wait(ms, signal) {
161
+ return new Promise((resolve, reject) => {
162
+ function onAbort() {
163
+ clearTimeout(timer);
164
+ reject(signal.reason ?? new Error('Aborted'));
165
+ }
166
+ const timer = setTimeout(() => {
167
+ signal?.removeEventListener('abort', onAbort);
168
+ resolve();
169
+ }, ms);
170
+ if (signal) {
171
+ if (signal.aborted) {
172
+ clearTimeout(timer);
173
+ reject(signal.reason ?? new Error('Aborted'));
174
+ return;
175
+ }
176
+ signal.addEventListener('abort', onAbort, { once: true });
177
+ }
178
+ });
179
+ }
180
+ /**
181
+ * List the plans available for an add-on service, sorted ascending by
182
+ * `price.cents`. Plans without a price (or with equal prices) are
183
+ * returned later in the list.
184
+ */
185
+ export async function listPlans(ctx, serviceIdentity, options = {}) {
186
+ options.signal?.throwIfAborted();
187
+ const plans = await ctx.platform.plan.listByAddOn(serviceIdentity);
188
+ return [...plans].sort(byPriceCentsAsc);
189
+ }
190
+ function byPriceCentsAsc(a, b) {
191
+ return sortableCents(a) - sortableCents(b);
192
+ }
193
+ function sortableCents(plan) {
194
+ const cents = plan.price?.cents;
195
+ return typeof cents === 'number' ? cents : Number.MAX_SAFE_INTEGER;
196
+ }
197
+ /**
198
+ * Resolve, describe, and decorate an add-on for display.
199
+ *
200
+ * - Resolves the add-on by identity (optionally scoped to an app).
201
+ * - Loads the add-on's attachments.
202
+ * - Returns a copy with `plan.price` set from `billed_price` (so the
203
+ * value reflects any grandfathered/contract pricing).
204
+ *
205
+ * If `appIdentity` is provided and the platform returns 404 (resource
206
+ * `add_on`), the resolve falls back to a global lookup. This handles
207
+ * the case where the add-on belongs to a different app than the one
208
+ * supplied.
209
+ *
210
+ * Requests the `version=3.sdk` accept variant with `addon_service,plan`
211
+ * expansion so the resolved add-on includes the full `Plan` shape
212
+ * (`price.unit`, etc.) needed to render pricing.
213
+ */
214
+ export async function describeAddon(ctx, addonIdentity, options = {}) {
215
+ options.signal?.throwIfAborted();
216
+ // Accept-Expansion is scoped to the resolve call (addOn.resolution
217
+ // serializer accepts addon_service + plan expansions). The
218
+ // /addons/:id/addon-attachments endpoint uses Accept-Inclusion, not
219
+ // Accept-Expansion, and rejects an unknown expansion header.
220
+ const resolvingPlatform = ctx.platform.withHeaders({
221
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
222
+ 'Accept-Expansion': 'addon_service,plan',
223
+ });
224
+ const addon = await resolveAddonInternal(resolvingPlatform, addonIdentity, {
225
+ addonService: options.addonService,
226
+ appIdentity: options.appIdentity,
227
+ });
228
+ options.signal?.throwIfAborted();
229
+ const attachments = await ctx.platform.addOnAttachment.listByAddOn(addon.id);
230
+ const plan = addon.plan;
231
+ return {
232
+ ...addon,
233
+ ...(plan && { plan: { ...plan, price: grandfatheredPrice(addon) } }),
234
+ attachments,
235
+ };
236
+ }
237
+ /**
238
+ * Resolve a Platform add-on by identity.
239
+ *
240
+ * The add-on identity may be:
241
+ * - a UUID (`d5e3f2a4-...`)
242
+ * - a globally-unique name (`postgres-curved-12345`)
243
+ * - a namespaced credential reference (`postgres-curved-12345::SECONDARY`)
244
+ *
245
+ * When `appIdentity` is provided, the resolve is scoped to that app first
246
+ * and falls back to a global resolve if the platform returns 404 add_on.
247
+ * Namespaced identities (containing `::`) skip the app-scoped lookup
248
+ * because they are globally unique.
249
+ *
250
+ * `addonService` (e.g. `heroku-postgresql`) is filtered client-side after
251
+ * the resolve. The platform's server-side `addon_service` filter excludes
252
+ * alpha add-ons, so we don't pass it through.
253
+ *
254
+ * Errors:
255
+ * - throws `AddonNotFoundError` if no match is found
256
+ * - throws `AddonAmbiguousError` if multiple matches remain after filtering
257
+ *
258
+ * For attachment-based resolution (e.g. `DATABASE_URL` on a particular
259
+ * app), use `resolveAddonByAttachment` instead.
260
+ */
261
+ export async function resolveAddon(ctx, addonIdentity, options = {}) {
262
+ options.signal?.throwIfAborted();
263
+ return resolveAddonInternal(ctx.platform, addonIdentity, options);
264
+ }
265
+ /**
266
+ * Resolve a Platform add-on via one of its attachments on a given app.
267
+ *
268
+ * Use this when you have an attachment name (e.g. `DATABASE_URL`,
269
+ * `HEROKU_POSTGRESQL_GREEN`) on a known app, rather than an add-on
270
+ * identity. Calls `addOnAttachment.resolution` and returns the add-on
271
+ * the matched attachment points to.
272
+ *
273
+ * For resolving by add-on identity, use `resolveAddon`.
274
+ */
275
+ export async function resolveAddonByAttachment(ctx, appIdentity, attachmentName, options = {}) {
276
+ options.signal?.throwIfAborted();
277
+ debug('resolveByAttachment app=%s attachment=%s', appIdentity, attachmentName);
278
+ const matches = await ctx.platform.addOnAttachment.resolution({
279
+ // eslint-disable-next-line camelcase
280
+ addon_attachment: attachmentName,
281
+ app: appIdentity,
282
+ });
283
+ const attachment = matches[0];
284
+ const addon = attachment?.addon;
285
+ if (!addon?.id || !addon.app?.id) {
286
+ debug('resolveByAttachment matches=%d (no usable add-on returned)', matches.length);
287
+ throw new AddonNotFoundError();
288
+ }
289
+ debug('resolveByAttachment resolved addon=%s app=%s', addon.id, addon.app.id);
290
+ return addon;
291
+ }
292
+ async function resolveAddonInternal(platform, addonIdentity, options = {}) {
293
+ const { addonService, appIdentity } = options;
294
+ const resolveBy = async (app) => {
295
+ const body = app ? { addon: addonIdentity, app } : { addon: addonIdentity };
296
+ debug('resolve addon=%s app=%s service=%s', addonIdentity, app ?? '<global>', addonService ?? '<any>');
297
+ const matches = await platform.addOn.resolution(body);
298
+ const filtered = addonService
299
+ ? matches.filter(addon => addon.addon_service?.name === addonService)
300
+ : matches;
301
+ debug('resolve matches=%d filtered=%d (service=%s)', matches.length, filtered.length, addonService ?? '<any>');
302
+ const resolvedAddon = singularize(filtered);
303
+ debug('resolve resolved addon=%s app=%s', resolvedAddon.id, resolvedAddon.app.id);
304
+ return resolvedAddon;
305
+ };
306
+ if (!appIdentity || addonIdentity.includes('::')) {
307
+ debug('resolve scope=global reason=%s', appIdentity ? 'namespaced-identity' : 'no-app');
308
+ return resolveBy();
309
+ }
310
+ try {
311
+ return await resolveBy(appIdentity);
312
+ }
313
+ catch (error) {
314
+ if (isAddOnNotFound(error)) {
315
+ debug('resolve app-scope 404 add_on, falling back to global addon=%s', addonIdentity);
316
+ return resolveBy();
317
+ }
318
+ throw error;
319
+ }
320
+ }
321
+ function isAddOnNotFound(error) {
322
+ return error instanceof NotFoundError && error.resource === 'add_on';
323
+ }
324
+ function singularize(matches) {
325
+ if (matches.length === 0) {
326
+ throw new AddonNotFoundError();
327
+ }
328
+ if (matches.length > 1) {
329
+ throw new AddonAmbiguousError(matches);
330
+ }
331
+ const match = matches[0];
332
+ if (!match.id || !match.app?.id) {
333
+ throw new Error(`Resolved add-on is missing required fields (id=${match.id}, app.id=${match.app?.id})`);
334
+ }
335
+ return match;
336
+ }
337
+ function grandfatheredPrice(addon) {
338
+ const price = addon.plan?.price;
339
+ return {
340
+ ...price,
341
+ ...(addon.billed_price && {
342
+ cents: addon.billed_price.cents,
343
+ contract: addon.billed_price.contract,
344
+ }),
345
+ };
346
+ }
347
+ export const addOnExtensions = extendResource('platform', 'addOn', ctx => ({
348
+ createAndWait: (appIdentity, body, options) => createAndWait(ctx, appIdentity, body, options),
349
+ describe: (addonIdentity, options) => describeAddon(ctx, addonIdentity, options),
350
+ listPlans: (serviceIdentity, options) => listPlans(ctx, serviceIdentity, options),
351
+ resolve: (addonIdentity, options) => resolveAddon(ctx, addonIdentity, options),
352
+ resolveByAttachment: (appIdentity, attachmentName, options) => resolveAddonByAttachment(ctx, appIdentity, attachmentName, options),
353
+ upgrade: (addonIdentity, plan, options) => upgrade(ctx, addonIdentity, plan, options),
354
+ }));
@@ -0,0 +1,58 @@
1
+ import type { App } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { DescribeAppOptions } from './info.js';
4
+ export { type AppInfo, describeApp, type DescribeAppOptions, type PipelineCouplingDetail, } from './info.js';
5
+ export type AppMaintenanceOptions = {
6
+ signal?: AbortSignal;
7
+ };
8
+ export type GenerationKind = 'cedar' | 'fir';
9
+ export type GetGenerationOptions = {
10
+ signal?: AbortSignal;
11
+ };
12
+ export type IsShieldedOptions = {
13
+ signal?: AbortSignal;
14
+ };
15
+ export type GetProcessTierOptions = {
16
+ signal?: AbortSignal;
17
+ };
18
+ export type ProcessTier = 'basic' | 'eco' | 'performance' | 'private' | 'shield' | 'standard' | (string & {});
19
+ export declare function enableMaintenance(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: AppMaintenanceOptions): Promise<App>;
20
+ export declare function disableMaintenance(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: AppMaintenanceOptions): Promise<App>;
21
+ /**
22
+ * Look up the platform generation (`cedar` or `fir`) of an app.
23
+ *
24
+ * Issues `app.info` with the `version=3.sdk` Accept variant so the
25
+ * response includes the `generation` field, then normalizes the
26
+ * platform's strings to the `'cedar' | 'fir'` union. Returns
27
+ * `undefined` if the platform reports an unrecognized value (e.g. a
28
+ * future generation name we haven't taught the SDK about).
29
+ */
30
+ export declare function getGeneration(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: GetGenerationOptions): Promise<GenerationKind | undefined>;
31
+ type AppInfoResult = App & {
32
+ process_tier?: string;
33
+ space?: null | {
34
+ shield?: boolean;
35
+ };
36
+ };
37
+ type AppInfoPlatform = {
38
+ app: {
39
+ info(appIdentity: string): Promise<AppInfoResult>;
40
+ };
41
+ withOptions(opts: {
42
+ signal: AbortSignal;
43
+ }): AppInfoPlatform;
44
+ };
45
+ export declare function isShielded(ctx: {
46
+ platform: AppInfoPlatform;
47
+ }, appIdentity: string, options?: IsShieldedOptions): Promise<boolean>;
48
+ export declare function getProcessTier(ctx: {
49
+ platform: AppInfoPlatform;
50
+ }, appIdentity: string, options?: GetProcessTierOptions): Promise<ProcessTier | undefined>;
51
+ export declare const appExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "app", {
52
+ describe: (appIdentity: string, options?: DescribeAppOptions) => Promise<import("./info.js").AppInfo>;
53
+ disableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
54
+ enableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
55
+ getGeneration: (appIdentity: string, options?: GetGenerationOptions) => Promise<GenerationKind | undefined>;
56
+ getProcessTier: (appIdentity: string, options?: GetProcessTierOptions) => Promise<ProcessTier | undefined>;
57
+ isShielded: (appIdentity: string, options?: IsShieldedOptions) => Promise<boolean>;
58
+ }>;
@@ -0,0 +1,53 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { describeApp } from './info.js';
3
+ export { describeApp, } from './info.js';
4
+ export async function enableMaintenance(ctx, appIdentity, options = {}) {
5
+ options.signal?.throwIfAborted();
6
+ return ctx.platform.app.update(appIdentity, { maintenance: true });
7
+ }
8
+ export async function disableMaintenance(ctx, appIdentity, options = {}) {
9
+ options.signal?.throwIfAborted();
10
+ return ctx.platform.app.update(appIdentity, { maintenance: false });
11
+ }
12
+ /**
13
+ * Look up the platform generation (`cedar` or `fir`) of an app.
14
+ *
15
+ * Issues `app.info` with the `version=3.sdk` Accept variant so the
16
+ * response includes the `generation` field, then normalizes the
17
+ * platform's strings to the `'cedar' | 'fir'` union. Returns
18
+ * `undefined` if the platform reports an unrecognized value (e.g. a
19
+ * future generation name we haven't taught the SDK about).
20
+ */
21
+ export async function getGeneration(ctx, appIdentity, options = {}) {
22
+ options.signal?.throwIfAborted();
23
+ const platform = ctx.platform.withHeaders({
24
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
25
+ });
26
+ const app = await platform.app.info(appIdentity);
27
+ return parseGeneration(app.generation);
28
+ }
29
+ function parseGeneration(value) {
30
+ if (value === 'cedar' || value === 'fir')
31
+ return value;
32
+ return undefined;
33
+ }
34
+ export async function isShielded(ctx, appIdentity, options = {}) {
35
+ options.signal?.throwIfAborted();
36
+ const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
37
+ const app = await platform.app.info(appIdentity);
38
+ return Boolean(app.space?.shield);
39
+ }
40
+ export async function getProcessTier(ctx, appIdentity, options = {}) {
41
+ options.signal?.throwIfAborted();
42
+ const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
43
+ const app = await platform.app.info(appIdentity);
44
+ return app.process_tier;
45
+ }
46
+ export const appExtensions = extendResource('platform', 'app', ctx => ({
47
+ describe: (appIdentity, options) => describeApp(ctx, appIdentity, options),
48
+ disableMaintenance: (appIdentity, options) => disableMaintenance(ctx, appIdentity, options),
49
+ enableMaintenance: (appIdentity, options) => enableMaintenance(ctx, appIdentity, options),
50
+ getGeneration: (appIdentity, options) => getGeneration(ctx, appIdentity, options),
51
+ getProcessTier: (appIdentity, options) => getProcessTier(ctx, appIdentity, options),
52
+ isShielded: (appIdentity, options) => isShielded(ctx, appIdentity, options),
53
+ }));
@@ -0,0 +1,30 @@
1
+ import type { AddOn, App, Collaborator, Dyno, PipelineCoupling } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ export type DescribeAppOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export type PipelineCouplingDetail = PipelineCoupling & {
7
+ pipeline: {
8
+ id?: string;
9
+ name: string;
10
+ };
11
+ stage: 'development' | 'production' | 'review' | 'staging' | 'test';
12
+ };
13
+ export type AppInfo = {
14
+ addons: AddOn[];
15
+ app: App;
16
+ collaborators: Collaborator[];
17
+ dynos: Dyno[];
18
+ pipelineCoupling: null | PipelineCouplingDetail;
19
+ };
20
+ /**
21
+ * Composite "describe this app" view: parallel-fetches add-ons, the app
22
+ * itself, dynos, collaborators, and the pipeline coupling.
23
+ *
24
+ * Hard-fails on add-ons or app (the response is meaningless without
25
+ * either). Soft-fails on dynos, collaborators, and pipeline coupling —
26
+ * any error collapses to an empty default. The broad .catch is
27
+ * intentional v1 fidelity with the Heroku CLI's apps:info; a future
28
+ * tightening to specific status codes is non-breaking.
29
+ */
30
+ export declare function describeApp(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: DescribeAppOptions): Promise<AppInfo>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Composite "describe this app" view: parallel-fetches add-ons, the app
3
+ * itself, dynos, collaborators, and the pipeline coupling.
4
+ *
5
+ * Hard-fails on add-ons or app (the response is meaningless without
6
+ * either). Soft-fails on dynos, collaborators, and pipeline coupling —
7
+ * any error collapses to an empty default. The broad .catch is
8
+ * intentional v1 fidelity with the Heroku CLI's apps:info; a future
9
+ * tightening to specific status codes is non-breaking.
10
+ */
11
+ export async function describeApp(ctx, appIdentity, options = {}) {
12
+ options.signal?.throwIfAborted();
13
+ const platform = options.signal
14
+ ? ctx.platform.withOptions({ signal: options.signal })
15
+ : ctx.platform;
16
+ const [addons, app, dynos, collaborators, pipelineCoupling] = await Promise.all([
17
+ platform.addOn.listByApp(appIdentity),
18
+ platform.app.info(appIdentity),
19
+ platform.dyno.list(appIdentity).catch(() => []),
20
+ platform.collaborator.list(appIdentity).catch(() => []),
21
+ platform.pipelineCoupling.infoByApp(appIdentity)
22
+ .catch(() => null),
23
+ ]);
24
+ return {
25
+ addons, app, collaborators, dynos, pipelineCoupling,
26
+ };
27
+ }
@@ -0,0 +1,56 @@
1
+ import type { App } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../core/extend-resource.js';
3
+ export type AppMaintenanceOptions = {
4
+ signal?: AbortSignal;
5
+ };
6
+ export type GenerationKind = 'cedar' | 'fir';
7
+ export type GetGenerationOptions = {
8
+ signal?: AbortSignal;
9
+ };
10
+ export type IsShieldedOptions = {
11
+ signal?: AbortSignal;
12
+ };
13
+ export type GetProcessTierOptions = {
14
+ signal?: AbortSignal;
15
+ };
16
+ export type ProcessTier = 'eco' | 'basic' | 'standard' | 'performance' | 'private' | 'shield' | (string & {});
17
+ export declare function enableMaintenance(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: AppMaintenanceOptions): Promise<App>;
18
+ export declare function disableMaintenance(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: AppMaintenanceOptions): Promise<App>;
19
+ /**
20
+ * Look up the platform generation (`cedar` or `fir`) of an app.
21
+ *
22
+ * Issues `app.info` with the `version=3.sdk` Accept variant so the
23
+ * response includes the `generation` field, then normalizes the
24
+ * platform's strings to the `'cedar' | 'fir'` union. Returns
25
+ * `undefined` if the platform reports an unrecognized value (e.g. a
26
+ * future generation name we haven't taught the SDK about).
27
+ */
28
+ export declare function getGeneration(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: GetGenerationOptions): Promise<GenerationKind | undefined>;
29
+ type AppInfoResult = App & {
30
+ process_tier?: string;
31
+ space?: {
32
+ shield?: boolean;
33
+ } | null;
34
+ };
35
+ type AppInfoPlatform = {
36
+ app: {
37
+ info(appIdentity: string): Promise<AppInfoResult>;
38
+ };
39
+ withOptions(opts: {
40
+ signal: AbortSignal;
41
+ }): AppInfoPlatform;
42
+ };
43
+ export declare function isShielded(ctx: {
44
+ platform: AppInfoPlatform;
45
+ }, appIdentity: string, options?: IsShieldedOptions): Promise<boolean>;
46
+ export declare function getProcessTier(ctx: {
47
+ platform: AppInfoPlatform;
48
+ }, appIdentity: string, options?: GetProcessTierOptions): Promise<ProcessTier | undefined>;
49
+ export declare const appExtensions: import("../../index.js").ResourceExtension<"platform", "app", {
50
+ disableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
51
+ enableMaintenance: (appIdentity: string, options?: AppMaintenanceOptions) => Promise<App>;
52
+ getGeneration: (appIdentity: string, options?: GetGenerationOptions) => Promise<GenerationKind | undefined>;
53
+ getProcessTier: (appIdentity: string, options?: GetProcessTierOptions) => Promise<ProcessTier | undefined>;
54
+ isShielded: (appIdentity: string, options?: IsShieldedOptions) => Promise<boolean>;
55
+ }>;
56
+ export {};
@@ -0,0 +1,50 @@
1
+ import { extendResource } from '../../core/extend-resource.js';
2
+ export async function enableMaintenance(ctx, appIdentity, options = {}) {
3
+ options.signal?.throwIfAborted();
4
+ return ctx.platform.app.update(appIdentity, { maintenance: true });
5
+ }
6
+ export async function disableMaintenance(ctx, appIdentity, options = {}) {
7
+ options.signal?.throwIfAborted();
8
+ return ctx.platform.app.update(appIdentity, { maintenance: false });
9
+ }
10
+ /**
11
+ * Look up the platform generation (`cedar` or `fir`) of an app.
12
+ *
13
+ * Issues `app.info` with the `version=3.sdk` Accept variant so the
14
+ * response includes the `generation` field, then normalizes the
15
+ * platform's strings to the `'cedar' | 'fir'` union. Returns
16
+ * `undefined` if the platform reports an unrecognized value (e.g. a
17
+ * future generation name we haven't taught the SDK about).
18
+ */
19
+ export async function getGeneration(ctx, appIdentity, options = {}) {
20
+ options.signal?.throwIfAborted();
21
+ const platform = ctx.platform.withHeaders({
22
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
23
+ });
24
+ const app = await platform.app.info(appIdentity);
25
+ return parseGeneration(app.generation);
26
+ }
27
+ function parseGeneration(value) {
28
+ if (value === 'cedar' || value === 'fir')
29
+ return value;
30
+ return undefined;
31
+ }
32
+ export async function isShielded(ctx, appIdentity, options = {}) {
33
+ options.signal?.throwIfAborted();
34
+ const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
35
+ const app = await platform.app.info(appIdentity);
36
+ return Boolean(app.space?.shield);
37
+ }
38
+ export async function getProcessTier(ctx, appIdentity, options = {}) {
39
+ options.signal?.throwIfAborted();
40
+ const platform = options.signal ? ctx.platform.withOptions({ signal: options.signal }) : ctx.platform;
41
+ const app = await platform.app.info(appIdentity);
42
+ return app.process_tier;
43
+ }
44
+ export const appExtensions = extendResource('platform', 'app', ctx => ({
45
+ disableMaintenance: (appIdentity, options) => disableMaintenance(ctx, appIdentity, options),
46
+ enableMaintenance: (appIdentity, options) => enableMaintenance(ctx, appIdentity, options),
47
+ getGeneration: (appIdentity, options) => getGeneration(ctx, appIdentity, options),
48
+ getProcessTier: (appIdentity, options) => getProcessTier(ctx, appIdentity, options),
49
+ isShielded: (appIdentity, options) => isShielded(ctx, appIdentity, options),
50
+ }));
@@ -0,0 +1,45 @@
1
+ import type { Formation } from '@heroku/types/3.sdk';
2
+ import type { StickyRouteOptions } from '../../../core/create-client.js';
3
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
4
+ export type DynoOptions = {
5
+ signal?: AbortSignal;
6
+ };
7
+ export type ScaleDynosUpdate = {
8
+ dyno_size?: {
9
+ id?: string;
10
+ name?: string;
11
+ };
12
+ quantity?: number | string;
13
+ size?: string;
14
+ type: string;
15
+ };
16
+ export type RestartDynosTarget = {
17
+ dyno: string;
18
+ } | {
19
+ type: string;
20
+ };
21
+ type FormationClient = {
22
+ batchUpdate(appIdentity: string, body: {
23
+ updates: ScaleDynosUpdate[];
24
+ }): Promise<Formation[]>;
25
+ update(appIdentity: string, type: string, body: Omit<ScaleDynosUpdate, 'type'>): Promise<Formation>;
26
+ };
27
+ type ScalePlatform = {
28
+ formation: FormationClient;
29
+ withOptions(opts: StickyRouteOptions): ScalePlatform;
30
+ };
31
+ export declare function scaleDynos(ctx: {
32
+ platform: ScalePlatform;
33
+ }, appIdentity: string, updates: ScaleDynosUpdate, options?: DynoOptions): Promise<Formation>;
34
+ export declare function scaleDynos(ctx: {
35
+ platform: ScalePlatform;
36
+ }, appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
37
+ export declare function restartDynos(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions): Promise<void>;
38
+ export declare const dynoExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "dyno", {
39
+ restart: (appIdentity: string, target?: RestartDynosTarget, options?: DynoOptions) => Promise<void>;
40
+ scale: {
41
+ (appIdentity: string, updates: ScaleDynosUpdate, options?: DynoOptions): Promise<Formation>;
42
+ (appIdentity: string, updates: ScaleDynosUpdate[], options?: DynoOptions): Promise<Formation[]>;
43
+ };
44
+ }>;
45
+ export {};