@heroku/sdk 0.5.2 → 0.5.4

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 (53) hide show
  1. package/dist/core/create-client.d.ts +9 -1
  2. package/dist/core/create-client.js +4 -2
  3. package/dist/resources/data/redis/errors.d.ts +8 -0
  4. package/dist/resources/data/redis/errors.js +15 -0
  5. package/dist/resources/data/redis/index.d.ts +9 -0
  6. package/dist/resources/data/redis/index.js +10 -0
  7. package/dist/resources/data/redis/resolve-by-app.d.ts +35 -0
  8. package/dist/resources/data/redis/resolve-by-app.js +56 -0
  9. package/dist/resources/data/redis/wait-for-ready.d.ts +32 -0
  10. package/dist/resources/data/redis/wait-for-ready.js +46 -0
  11. package/dist/resources/extensions/data.d.ts +1 -0
  12. package/dist/resources/extensions/data.js +1 -0
  13. package/dist/resources/extensions/platform.d.ts +5 -1
  14. package/dist/resources/extensions/platform.js +5 -1
  15. package/dist/resources/platform/add-on/destroy-and-wait.d.ts +22 -0
  16. package/dist/resources/platform/add-on/destroy-and-wait.js +62 -0
  17. package/dist/resources/platform/add-on/index.d.ts +9 -5
  18. package/dist/resources/platform/add-on/index.js +6 -0
  19. package/dist/resources/platform/add-on/types.d.ts +35 -0
  20. package/dist/resources/platform/add-on/wait-for-provisioning.d.ts +19 -0
  21. package/dist/resources/platform/add-on/wait-for-provisioning.js +37 -0
  22. package/dist/resources/platform/app/index.d.ts +3 -0
  23. package/dist/resources/platform/app/index.js +3 -0
  24. package/dist/resources/platform/app/wait-for-acm-certificates.d.ts +34 -0
  25. package/dist/resources/platform/app/wait-for-acm-certificates.js +51 -0
  26. package/dist/resources/platform/container/ensure-container-stack.d.ts +18 -0
  27. package/dist/resources/platform/container/ensure-container-stack.js +32 -0
  28. package/dist/resources/platform/container/index.d.ts +13 -0
  29. package/dist/resources/platform/container/index.js +12 -0
  30. package/dist/resources/platform/container/release-images.d.ts +21 -0
  31. package/dist/resources/platform/container/release-images.js +23 -0
  32. package/dist/resources/platform/container/remove-process-types.d.ts +22 -0
  33. package/dist/resources/platform/container/remove-process-types.js +20 -0
  34. package/dist/resources/platform/dyno/index.d.ts +3 -0
  35. package/dist/resources/platform/dyno/index.js +3 -0
  36. package/dist/resources/platform/dyno/run.d.ts +76 -0
  37. package/dist/resources/platform/dyno/run.js +74 -0
  38. package/dist/resources/platform/log-session/index.js +109 -12
  39. package/dist/resources/platform/sni-endpoint/create-and-associate.d.ts +38 -0
  40. package/dist/resources/platform/sni-endpoint/create-and-associate.js +86 -0
  41. package/dist/resources/platform/sni-endpoint/index.d.ts +5 -0
  42. package/dist/resources/platform/sni-endpoint/index.js +6 -0
  43. package/dist/resources/platform/telemetry-drain/index.d.ts +6 -0
  44. package/dist/resources/platform/telemetry-drain/index.js +6 -0
  45. package/dist/resources/platform/telemetry-drain/remove-drains-for-target.d.ts +13 -0
  46. package/dist/resources/platform/telemetry-drain/remove-drains-for-target.js +25 -0
  47. package/dist/resources/platform/telemetry-drain/types.d.ts +13 -0
  48. package/dist/resources/platform/telemetry-drain/types.js +1 -0
  49. package/dist/resources/platform/test-run/index.d.ts +5 -0
  50. package/dist/resources/platform/test-run/index.js +9 -0
  51. package/dist/resources/platform/test-run/wait-for-state.d.ts +68 -0
  52. package/dist/resources/platform/test-run/wait-for-state.js +65 -0
  53. package/package.json +11 -3
@@ -0,0 +1,86 @@
1
+ import { waitForReady } from '../domain/wait-for-ready.js';
2
+ const DEFAULT_WAIT_INTERVAL_MS = 1000;
3
+ const DEFAULT_TIMEOUT_MS = 30_000;
4
+ /**
5
+ * Create an SNI endpoint (SSL certificate) on an app and associate it with the
6
+ * app's matching domains.
7
+ *
8
+ * Ports the CLI `certs:add` business logic into the SDK.
9
+ *
10
+ * Steps:
11
+ * 1. Create the SNI endpoint from the given certificate chain and private key.
12
+ * 2. Wait for the app's domains to reach ready status (replaces the old CNAME poll).
13
+ * The SDK waits on domain STATUS (`succeeded`/`none`) via `domain.waitForReady`
14
+ * rather than the CLI's old `cname`-defined readiness check, polling at a default
15
+ * 1000ms interval to mirror the CLI's `waitForDomains` cadence. `timeoutMs`
16
+ * defaults to 30s and is applied per pending domain (not a single global budget),
17
+ * so an app with several pending domains can wait longer than 30s overall.
18
+ * 3. List the (now stable) app domains and wildcard-match them against the
19
+ * certificate's `cert_domains`.
20
+ * 4. If any domains matched, invoke `resolveDomains` — the interactive selection
21
+ * hook the consumer supplies — to choose which matched hostnames to associate.
22
+ * 5. Associate the selected hostnames with the endpoint in parallel via
23
+ * `PATCH /apps/{app}/domains/{hostname}` (`domain.update`).
24
+ *
25
+ * If no domains match the certificate, neither `resolveDomains` nor any
26
+ * association call is made.
27
+ *
28
+ * @throws if the domain readiness wait times out or a domain fails, or if any
29
+ * association call rejects.
30
+ * @returns the created SNI endpoint.
31
+ */
32
+ // Signature mirrors the CLI `certs:add` / plan-locked public API:
33
+ // createAndAssociate(app, certChain, privateKey, {resolveDomains}).
34
+ // eslint-disable-next-line max-params
35
+ export async function createAndAssociate(ctx, appIdentity, certificateChain, privateKey, options) {
36
+ const { intervalMs = DEFAULT_WAIT_INTERVAL_MS, resolveDomains, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
37
+ signal?.throwIfAborted();
38
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
39
+ const sniEndpoint = await platform.sniEndpoint.create(appIdentity, {
40
+ certificate_chain: certificateChain,
41
+ private_key: privateKey,
42
+ });
43
+ await waitForReady(ctx, appIdentity, { signal, timeoutMs, waitIntervalMs: intervalMs });
44
+ const apiDomains = await platform.domain.list(appIdentity);
45
+ const appDomains = apiDomains.map(d => d.hostname);
46
+ const certDomains = (sniEndpoint.ssl_cert.cert_domains ?? []);
47
+ const matched = matchDomains(certDomains, appDomains);
48
+ if (matched.length > 0) {
49
+ const selected = await resolveDomains(matched);
50
+ await Promise.all(selected.map(hostname => platform.domain.update(appIdentity, hostname, { sni_endpoint: sniEndpoint.name })));
51
+ }
52
+ return sniEndpoint;
53
+ }
54
+ function splitDomains(domains) {
55
+ return domains.map(domain => [domain.slice(0, 1), domain.slice(1)]);
56
+ }
57
+ // Adapted from the CLI `certs/add.ts` matcher, but ADDS `^`/`$` anchors to fix a
58
+ // prefix-match bug: without them a wildcard cert like `*.example.com` would match
59
+ // hostnames that merely share a prefix (e.g. `www.example.com.evil.org`). Wildcard
60
+ // certs must not match such hostnames, so both ends of the pattern are anchored.
61
+ function createMatcherFromSplitDomain([firstChar, rest]) {
62
+ const matcherContents = [];
63
+ if (firstChar === '*') {
64
+ matcherContents.push(String.raw `^[\w\-]+`);
65
+ }
66
+ else {
67
+ matcherContents.push(`^${firstChar}`);
68
+ }
69
+ const escapedRest = rest.replaceAll('.', String.raw `\.`);
70
+ matcherContents.push(escapedRest);
71
+ return new RegExp(`${matcherContents.join('')}$`);
72
+ }
73
+ function matchDomains(certDomains, appDomains) {
74
+ const splitCertDomains = splitDomains(certDomains);
75
+ const matchers = splitCertDomains.map(splitDomain => createMatcherFromSplitDomain(splitDomain));
76
+ if (splitCertDomains.some(domain => (domain[0] === '*'))) {
77
+ const matchedDomains = [];
78
+ for (const appDomain of appDomains) {
79
+ if (matchers.some(matcher => matcher.test(appDomain))) {
80
+ matchedDomains.push(appDomain);
81
+ }
82
+ }
83
+ return matchedDomains;
84
+ }
85
+ return certDomains.filter(domain => appDomains.includes(domain));
86
+ }
@@ -0,0 +1,5 @@
1
+ import type { CreateAndAssociateOptions } from './create-and-associate.js';
2
+ export { createAndAssociate, type CreateAndAssociateOptions } from './create-and-associate.js';
3
+ export declare const sniEndpointExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "sniEndpoint", {
4
+ createAndAssociate: (appIdentity: string, certificateChain: string, privateKey: string, options: CreateAndAssociateOptions) => Promise<import("@heroku/types/3.sdk").SniEndpoint>;
5
+ }>;
@@ -0,0 +1,6 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { createAndAssociate } from './create-and-associate.js';
3
+ export { createAndAssociate } from './create-and-associate.js';
4
+ export const sniEndpointExtensions = extendResource('platform', 'sniEndpoint', ctx => ({
5
+ createAndAssociate: (appIdentity, certificateChain, privateKey, options) => createAndAssociate(ctx, appIdentity, certificateChain, privateKey, options),
6
+ }));
@@ -0,0 +1,6 @@
1
+ import type { TelemetryDrainOptions, TelemetryDrainTarget } from './types.js';
2
+ export { removeDrainsForTarget } from './remove-drains-for-target.js';
3
+ export type { TelemetryDrainOptions, TelemetryDrainTarget } from './types.js';
4
+ export declare const telemetryDrainExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "telemetryDrain", {
5
+ removeDrains: (target: TelemetryDrainTarget, options?: TelemetryDrainOptions) => Promise<import("@heroku/types/3.sdk").TelemetryDrain[]>;
6
+ }>;
@@ -0,0 +1,6 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { removeDrainsForTarget } from './remove-drains-for-target.js';
3
+ export { removeDrainsForTarget } from './remove-drains-for-target.js';
4
+ export const telemetryDrainExtensions = extendResource('platform', 'telemetryDrain', ctx => ({
5
+ removeDrains: (target, options) => removeDrainsForTarget(ctx, target, options),
6
+ }));
@@ -0,0 +1,13 @@
1
+ import type { TelemetryDrain } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { TelemetryDrainOptions, TelemetryDrainTarget } from './types.js';
4
+ /**
5
+ * Remove all telemetry drains for a given app or space.
6
+ *
7
+ * Behavior:
8
+ * - Dispatches to listByApp or listBySpace based on target discriminant
9
+ * - Deletes all drains in parallel via Promise.all
10
+ * - Returns empty array if no drains exist
11
+ * - Rejects if any individual delete fails
12
+ */
13
+ export declare function removeDrainsForTarget(ctx: Pick<ResourceCtx, 'platform'>, target: TelemetryDrainTarget, options?: TelemetryDrainOptions): Promise<TelemetryDrain[]>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Remove all telemetry drains for a given app or space.
3
+ *
4
+ * Behavior:
5
+ * - Dispatches to listByApp or listBySpace based on target discriminant
6
+ * - Deletes all drains in parallel via Promise.all
7
+ * - Returns empty array if no drains exist
8
+ * - Rejects if any individual delete fails
9
+ */
10
+ export async function removeDrainsForTarget(ctx, target, options = {}) {
11
+ const { signal } = options;
12
+ const { app, space } = target;
13
+ signal?.throwIfAborted();
14
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
15
+ let drains = [];
16
+ if (app) {
17
+ drains = await platform.telemetryDrain.listByApp(app);
18
+ }
19
+ else if (space) {
20
+ drains = await platform.telemetryDrain.listBySpace(space);
21
+ }
22
+ signal?.throwIfAborted();
23
+ const deletedDrains = await Promise.all(drains.map(drain => platform.telemetryDrain.delete(drain.id)));
24
+ return deletedDrains;
25
+ }
@@ -0,0 +1,13 @@
1
+ export type TelemetryDrainOptions = {
2
+ /**
3
+ * Abort signal to cancel the operation.
4
+ */
5
+ signal?: AbortSignal;
6
+ };
7
+ export type TelemetryDrainTarget = {
8
+ app: string;
9
+ space?: never;
10
+ } | {
11
+ app?: never;
12
+ space: string;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { type TestRunStatus, type WaitForStateOptions } from './wait-for-state.js';
2
+ export { TestRunNotReadyError, type TestRunStatus, waitForState, type WaitForStateOptions, } from './wait-for-state.js';
3
+ export declare const testRunExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "testRun", {
4
+ waitForState: (pipelineId: string, testRunNumber: number, targetStates: ReadonlyArray<string | TestRunStatus>, options?: WaitForStateOptions) => Promise<import("@heroku/types/3.sdk").TestRun>;
5
+ }>;
@@ -0,0 +1,9 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { waitForState } from './wait-for-state.js';
3
+ export { TestRunNotReadyError, waitForState, } from './wait-for-state.js';
4
+ // Resource key MUST be the camelCase `'testRun'` — it matches the generated
5
+ // route-group export name that `mergeExtensions` looks up on the platform
6
+ // client. The directory stays kebab (`test-run/`).
7
+ export const testRunExtensions = extendResource('platform', 'testRun', ctx => ({
8
+ waitForState: (pipelineId, testRunNumber, targetStates, options) => waitForState(ctx, pipelineId, testRunNumber, targetStates, options),
9
+ }));
@@ -0,0 +1,68 @@
1
+ import type { TestRun } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ /**
4
+ * The documented `TestRun.status` values from the Platform API. Provided
5
+ * for autocomplete in {@link waitForState}'s `targetStates`; the runtime
6
+ * comparison is plain string membership (`targetStates.includes(run.status)`),
7
+ * so any string is accepted.
8
+ */
9
+ export type TestRunStatus = TestRun['status'];
10
+ /**
11
+ * Thrown by {@link waitForState} when the test run did not reach one of
12
+ * `targetStates` within `timeoutMs`. Carries the last observed run so
13
+ * callers can branch on its terminal `status` (e.g. distinguish a
14
+ * `failed` run from one still `building`). Mirrors `DynoNotReadyError`.
15
+ */
16
+ export declare class TestRunNotReadyError extends Error {
17
+ readonly testRun: TestRun;
18
+ readonly expectedStates: ReadonlyArray<string>;
19
+ readonly timeoutMs: number;
20
+ readonly id = "test_run_not_ready";
21
+ constructor(testRun: TestRun, expectedStates: ReadonlyArray<string>, timeoutMs: number);
22
+ }
23
+ export type WaitForStateOptions = {
24
+ /**
25
+ * Delay between polls in milliseconds. Defaults to 1000.
26
+ */
27
+ intervalMs?: number;
28
+ /**
29
+ * Fires after every successful `testRun.infoByPipeline` call, including
30
+ * runs whose `status` does not yet match `targetStates` (and trigger
31
+ * another poll) AND the final matching run before it is returned. Use
32
+ * this to surface live state transitions in a UI. Exceptions thrown from
33
+ * this callback propagate and end the wait — there is no implicit catch.
34
+ */
35
+ onPoll?: (run: TestRun) => void;
36
+ /**
37
+ * Cancels the wait early. If already aborted, {@link waitForState}
38
+ * rejects before the first poll; if aborted mid-wait, the pending delay
39
+ * rejects with `signal.reason`.
40
+ */
41
+ signal?: AbortSignal;
42
+ /**
43
+ * Wall-clock budget in milliseconds before {@link TestRunNotReadyError}
44
+ * is thrown. Defaults to 600000 (10 minutes) — a finite default so a
45
+ * stuck run can't hang forever. Pass a larger value for long CI runs.
46
+ */
47
+ timeoutMs?: number;
48
+ };
49
+ /**
50
+ * Poll `testRun.infoByPipeline` until the run's `status` is one of
51
+ * `targetStates`, then return that run. Reuses the SDK's cancellable
52
+ * `wait()` helper to space polls — no bare `setTimeout` loop.
53
+ *
54
+ * Transient-error handling: errors from `infoByPipeline` **propagate**;
55
+ * there is no 404/429/503 retry. This matches the closest precedent,
56
+ * `promotePipeline`, and the original CLI `waitForStates`, which had no
57
+ * retry either — adopting `waitForInfo`'s transient retry would be behavior
58
+ * the eventual CLI caller never had.
59
+ *
60
+ * On exceeding `timeoutMs` without a match, throws {@link TestRunNotReadyError}
61
+ * carrying the last observed run.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const run = await platform.testRun.waitForState(pipelineId, 42, ['debugging', 'errored'])
66
+ * ```
67
+ */
68
+ export declare function waitForState(ctx: Pick<ResourceCtx, 'platform'>, pipelineId: string, testRunNumber: number, targetStates: ReadonlyArray<string | TestRunStatus>, options?: WaitForStateOptions): Promise<TestRun>;
@@ -0,0 +1,65 @@
1
+ import { wait } from '../../../utils/wait.js';
2
+ const DEFAULT_INTERVAL_MS = 1000;
3
+ // Finite default so a stuck CI test run can't hang a caller forever.
4
+ // Diverges deliberately from `promotePipeline`, which leaves `timeoutMs`
5
+ // unbounded; callers that need longer can pass an explicit `timeoutMs`.
6
+ const DEFAULT_TIMEOUT_MS = 600_000;
7
+ /**
8
+ * Thrown by {@link waitForState} when the test run did not reach one of
9
+ * `targetStates` within `timeoutMs`. Carries the last observed run so
10
+ * callers can branch on its terminal `status` (e.g. distinguish a
11
+ * `failed` run from one still `building`). Mirrors `DynoNotReadyError`.
12
+ */
13
+ export class TestRunNotReadyError extends Error {
14
+ testRun;
15
+ expectedStates;
16
+ timeoutMs;
17
+ id = 'test_run_not_ready';
18
+ constructor(testRun, expectedStates, timeoutMs) {
19
+ super(`Test run #${testRun.number} did not reach status ${expectedStates.join('/')} `
20
+ + `within ${timeoutMs}ms (last status: ${testRun.status}).`);
21
+ this.testRun = testRun;
22
+ this.expectedStates = expectedStates;
23
+ this.timeoutMs = timeoutMs;
24
+ this.name = 'TestRunNotReadyError';
25
+ }
26
+ }
27
+ /**
28
+ * Poll `testRun.infoByPipeline` until the run's `status` is one of
29
+ * `targetStates`, then return that run. Reuses the SDK's cancellable
30
+ * `wait()` helper to space polls — no bare `setTimeout` loop.
31
+ *
32
+ * Transient-error handling: errors from `infoByPipeline` **propagate**;
33
+ * there is no 404/429/503 retry. This matches the closest precedent,
34
+ * `promotePipeline`, and the original CLI `waitForStates`, which had no
35
+ * retry either — adopting `waitForInfo`'s transient retry would be behavior
36
+ * the eventual CLI caller never had.
37
+ *
38
+ * On exceeding `timeoutMs` without a match, throws {@link TestRunNotReadyError}
39
+ * carrying the last observed run.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const run = await platform.testRun.waitForState(pipelineId, 42, ['debugging', 'errored'])
44
+ * ```
45
+ */
46
+ export async function waitForState(ctx, pipelineId, testRunNumber, targetStates, options = {}) {
47
+ const { intervalMs = DEFAULT_INTERVAL_MS, onPoll, signal, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
48
+ signal?.throwIfAborted();
49
+ // Scope the in-flight info call to the caller's signal so an abort takes
50
+ // effect during the API call, not just during the backoff.
51
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
52
+ const deadline = Date.now() + timeoutMs;
53
+ while (true) {
54
+ signal?.throwIfAborted();
55
+ const run = await platform.testRun.infoByPipeline(pipelineId, testRunNumber);
56
+ onPoll?.(run);
57
+ if (targetStates.includes(run.status)) {
58
+ return run;
59
+ }
60
+ if (Date.now() >= deadline) {
61
+ throw new TestRunNotReadyError(run, targetStates, timeoutMs);
62
+ }
63
+ await wait(intervalMs, signal);
64
+ }
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroku/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Heroku SDK",
5
5
  "author": "Heroku",
6
6
  "type": "module",
@@ -50,6 +50,14 @@
50
50
  "types": "./dist/resources/platform/pipeline/*.d.ts",
51
51
  "default": "./dist/resources/platform/pipeline/*.js"
52
52
  },
53
+ "./resources/platform/test-run": {
54
+ "types": "./dist/resources/platform/test-run/index.d.ts",
55
+ "default": "./dist/resources/platform/test-run/index.js"
56
+ },
57
+ "./resources/data/redis": {
58
+ "types": "./dist/resources/data/redis/index.d.ts",
59
+ "default": "./dist/resources/data/redis/index.js"
60
+ },
53
61
  "./resources/*": {
54
62
  "types": "./dist/resources/*.d.ts",
55
63
  "default": "./dist/resources/*.js"
@@ -91,8 +99,8 @@
91
99
  "vitest": "^4.1.4"
92
100
  },
93
101
  "dependencies": {
94
- "@heroku/heroku-fetch": "^0.1.1-beta",
95
- "@heroku/types": "^3.0.0-beta",
102
+ "@heroku/heroku-fetch": "^0.1.3",
103
+ "@heroku/types": "^4.0.1",
96
104
  "debug": "^4.4.0"
97
105
  }
98
106
  }