@heroku/sdk 0.5.0 → 0.5.1

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.
@@ -1,5 +1,6 @@
1
1
  export { addOnExtensions } from '../platform/add-on/index.js';
2
2
  export { appExtensions, type AppInfo, type PipelineCouplingDetail } from '../platform/app/index.js';
3
+ export { type CreateAndWaitOptions, domainExtensions, type DomainOptions, type WaitForReadyOptions, } from '../platform/domain/index.js';
3
4
  export { dynoExtensions, DynoNotReadyError, type DynoState, waitForInfo, type WaitForInfoOptions, } from '../platform/dyno/index.js';
4
5
  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
6
  export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
@@ -1,5 +1,6 @@
1
1
  export { addOnExtensions } from '../platform/add-on/index.js';
2
2
  export { appExtensions } from '../platform/app/index.js';
3
+ export { domainExtensions, } from '../platform/domain/index.js';
3
4
  export { dynoExtensions, DynoNotReadyError, waitForInfo, } from '../platform/dyno/index.js';
4
5
  export { logSessionExtensions, parseHerokuLogLine, } from '../platform/log-session/index.js';
5
6
  export { pipelineCouplingExtensions } from '../platform/pipeline/coupling.js';
@@ -0,0 +1,9 @@
1
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
2
+ import type { DomainOptions } from './types.js';
3
+ /**
4
+ * Delete all custom domains from an app.
5
+ *
6
+ * Lists domains on the app, filters to kind === 'custom', and deletes
7
+ * each in sequence. Heroku-owned domains (kind === 'heroku') are skipped.
8
+ */
9
+ export declare function clearCustomDomains(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: DomainOptions): Promise<void>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Delete all custom domains from an app.
3
+ *
4
+ * Lists domains on the app, filters to kind === 'custom', and deletes
5
+ * each in sequence. Heroku-owned domains (kind === 'heroku') are skipped.
6
+ */
7
+ export async function clearCustomDomains(ctx, appIdentity, options = {}) {
8
+ const { signal } = options;
9
+ signal?.throwIfAborted();
10
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
11
+ // List and filter to custom domains
12
+ const domains = await platform.domain.list(appIdentity);
13
+ const customDomains = domains.filter(d => d.kind === 'custom');
14
+ // Delete each in sequence
15
+ for (const domain of customDomains) {
16
+ signal?.throwIfAborted();
17
+ await platform.domain.delete(appIdentity, domain.hostname);
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ import type { Domain } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { CreateAndWaitOptions } from './types.js';
4
+ /**
5
+ * Create a domain with SNI endpoint resolution and optional waiting.
6
+ *
7
+ * Steps:
8
+ * 1. If no explicit sniEndpoint option, list SNI endpoints on the app
9
+ * 2. If multiple SNI endpoints, call resolveSniEndpoint callback
10
+ * 3. Create the domain with SNI endpoint (defaults to null if not resolved)
11
+ * 4. If wait: true, poll until domain reaches ready status
12
+ */
13
+ export declare function createAndWait(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, hostname: string, options?: CreateAndWaitOptions): Promise<Domain>;
@@ -0,0 +1,46 @@
1
+ import { waitForReady } from './wait-for-ready.js';
2
+ /**
3
+ * Create a domain with SNI endpoint resolution and optional waiting.
4
+ *
5
+ * Steps:
6
+ * 1. If no explicit sniEndpoint option, list SNI endpoints on the app
7
+ * 2. If multiple SNI endpoints, call resolveSniEndpoint callback
8
+ * 3. Create the domain with SNI endpoint (defaults to null if not resolved)
9
+ * 4. If wait: true, poll until domain reaches ready status
10
+ */
11
+ export async function createAndWait(ctx, appIdentity, hostname, options = {}) {
12
+ const { resolveSniEndpoint, signal, sniEndpoint, timeoutMs, wait: shouldWait, waitIntervalMs, } = options;
13
+ signal?.throwIfAborted();
14
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
15
+ let resolvedSniEndpoint = null;
16
+ if (sniEndpoint) {
17
+ // Explicit SNI endpoint provided
18
+ resolvedSniEndpoint = sniEndpoint;
19
+ }
20
+ else {
21
+ // List SNI endpoints and resolve
22
+ const sniEndpoints = await platform.sniEndpoint.list(appIdentity);
23
+ if (sniEndpoints.length > 1) {
24
+ const sniEndpointSelection = await resolveSniEndpoint?.(sniEndpoints);
25
+ if (sniEndpointSelection) {
26
+ resolvedSniEndpoint = sniEndpointSelection;
27
+ }
28
+ }
29
+ }
30
+ // Create domain
31
+ let domain = await platform.domain.create(appIdentity, {
32
+ hostname,
33
+ // eslint-disable-next-line camelcase
34
+ sni_endpoint: resolvedSniEndpoint,
35
+ });
36
+ if (shouldWait && domain.status !== 'succeeded' && domain.status !== 'none') {
37
+ const [readyDomain] = await waitForReady(ctx, appIdentity, {
38
+ domain,
39
+ signal,
40
+ timeoutMs,
41
+ waitIntervalMs,
42
+ });
43
+ domain = readyDomain;
44
+ }
45
+ return domain;
46
+ }
@@ -0,0 +1,10 @@
1
+ import type { CreateAndWaitOptions, DomainOptions, WaitForReadyOptions } from './types.js';
2
+ export { clearCustomDomains } from './clear-custom-domains.js';
3
+ export { createAndWait } from './create-and-wait.js';
4
+ export type { CreateAndWaitOptions, DomainOptions, WaitForReadyOptions, } from './types.js';
5
+ export { waitForReady } from './wait-for-ready.js';
6
+ export declare const domainExtensions: import("../../../core/extend-resource.js").ResourceExtension<"platform", "domain", {
7
+ add: (appIdentity: string, hostname: string, options?: CreateAndWaitOptions) => Promise<import("@heroku/types/3.sdk").Domain>;
8
+ clear: (appIdentity: string, options?: DomainOptions) => Promise<void>;
9
+ wait: (appIdentity: string, options?: WaitForReadyOptions) => Promise<import("@heroku/types/3.sdk").Domain[]>;
10
+ }>;
@@ -0,0 +1,14 @@
1
+ import { extendResource } from '../../../core/extend-resource.js';
2
+ import { clearCustomDomains } from './clear-custom-domains.js';
3
+ import { createAndWait } from './create-and-wait.js';
4
+ import { waitForReady } from './wait-for-ready.js';
5
+ // Re-export implementations for direct use
6
+ export { clearCustomDomains } from './clear-custom-domains.js';
7
+ export { createAndWait } from './create-and-wait.js';
8
+ export { waitForReady } from './wait-for-ready.js';
9
+ // Extension definition
10
+ export const domainExtensions = extendResource('platform', 'domain', ctx => ({
11
+ add: (appIdentity, hostname, options) => createAndWait(ctx, appIdentity, hostname, options),
12
+ clear: (appIdentity, options) => clearCustomDomains(ctx, appIdentity, options),
13
+ wait: (appIdentity, options) => waitForReady(ctx, appIdentity, options),
14
+ }));
@@ -0,0 +1,24 @@
1
+ import type { Domain, SniEndpoint } from '@heroku/types/3.sdk';
2
+ import { vi } from 'vitest';
3
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
4
+ export type FakePlatform = {
5
+ domain: {
6
+ create: ReturnType<typeof vi.fn>;
7
+ delete: ReturnType<typeof vi.fn>;
8
+ info: ReturnType<typeof vi.fn>;
9
+ list: ReturnType<typeof vi.fn>;
10
+ };
11
+ sniEndpoint: {
12
+ list: ReturnType<typeof vi.fn>;
13
+ };
14
+ withOptions: ReturnType<typeof vi.fn>;
15
+ };
16
+ export declare function buildCtx(stubs?: {
17
+ domainCreate?: ReturnType<typeof vi.fn>;
18
+ domainDelete?: ReturnType<typeof vi.fn>;
19
+ domainInfo?: ReturnType<typeof vi.fn>;
20
+ domainList?: ReturnType<typeof vi.fn>;
21
+ sniEndpointList?: ReturnType<typeof vi.fn>;
22
+ }): ResourceCtx;
23
+ export declare function buildDomain(overrides?: Partial<Domain>): Domain;
24
+ export declare function buildSniEndpoint(overrides?: Partial<SniEndpoint>): SniEndpoint;
@@ -0,0 +1,60 @@
1
+ import { vi } from 'vitest';
2
+ export function buildCtx(stubs = {}) {
3
+ const platform = {
4
+ domain: {
5
+ create: stubs.domainCreate ?? vi.fn().mockResolvedValue({}),
6
+ delete: stubs.domainDelete ?? vi.fn().mockResolvedValue({}),
7
+ info: stubs.domainInfo ?? vi.fn().mockResolvedValue({}),
8
+ list: stubs.domainList ?? vi.fn().mockResolvedValue([]),
9
+ },
10
+ sniEndpoint: {
11
+ list: stubs.sniEndpointList ?? vi.fn().mockResolvedValue([]),
12
+ },
13
+ withOptions: vi.fn(function () {
14
+ return this;
15
+ }),
16
+ };
17
+ platform.withOptions.mockReturnValue(platform);
18
+ return {
19
+ data: {},
20
+ platform: platform,
21
+ };
22
+ }
23
+ export function buildDomain(overrides = {}) {
24
+ return {
25
+ acm_status: null,
26
+ acm_status_reason: null,
27
+ app: { id: 'app-123', name: 'test-app' },
28
+ cname: 'example.herokuapp.com',
29
+ created_at: '2024-01-01T00:00:00Z',
30
+ hostname: 'example.com',
31
+ id: 'domain-123',
32
+ kind: 'custom',
33
+ sni_endpoint: null,
34
+ status: 'pending',
35
+ updated_at: '2024-01-01T00:00:00Z',
36
+ ...overrides,
37
+ };
38
+ }
39
+ export function buildSniEndpoint(overrides = {}) {
40
+ return {
41
+ app: { id: 'app-123', name: 'test-app' },
42
+ certificate_chain: '-----BEGIN CERTIFICATE-----',
43
+ created_at: '2024-01-01T00:00:00Z',
44
+ display_name: 'example.com cert',
45
+ domains: ['example.com'],
46
+ id: 'sni-123',
47
+ name: 'tokyo-1234',
48
+ ssl_cert: {
49
+ 'ca_signed?': true,
50
+ cert_domains: ['example.com'],
51
+ expires_at: '2025-01-01T00:00:00Z',
52
+ issuer: 'Let\'s Encrypt',
53
+ 'self_signed?': false,
54
+ starts_at: '2024-01-01T00:00:00Z',
55
+ subject: 'CN=example.com',
56
+ },
57
+ updated_at: '2024-01-01T00:00:00Z',
58
+ ...overrides,
59
+ };
60
+ }
@@ -0,0 +1,58 @@
1
+ import type { Domain, SniEndpoint } from '@heroku/types/3.sdk';
2
+ export type DomainOptions = {
3
+ /**
4
+ * Abort signal to cancel the operation.
5
+ */
6
+ signal?: AbortSignal;
7
+ };
8
+ export type WaitForReadyOptions = DomainOptions & {
9
+ /**
10
+ * Domain to wait for.
11
+ */
12
+ domain?: Domain;
13
+ /**
14
+ * Hostname of the domain to wait for.
15
+ */
16
+ hostname?: string;
17
+ /**
18
+ * Maximum time in milliseconds to wait before giving up.
19
+ * If undefined, waits indefinitely (until signal aborted).
20
+ */
21
+ timeoutMs?: number;
22
+ /**
23
+ * Polling interval in milliseconds. Defaults to 5000.
24
+ */
25
+ waitIntervalMs?: number;
26
+ };
27
+ export type CreateAndWaitOptions = DomainOptions & {
28
+ /**
29
+ * Callback to resolve which SNI endpoint to use
30
+ * when one is not provided.
31
+ *
32
+ * @param candidates List of SNI endpoints to choose from
33
+ * @returns Promise resolving to the selected SNI endpoint
34
+ */
35
+ resolveSniEndpoint?: (candidates: SniEndpoint[]) => Promise<string> | string;
36
+ /**
37
+ * SNI endpoint to use.
38
+ *
39
+ * If provided, skips SNI endpoint listing and resolution.
40
+ */
41
+ sniEndpoint?: string;
42
+ /**
43
+ * Maximum time in milliseconds to wait when `wait` is true.
44
+ * Passed through to `waitForReady`.
45
+ */
46
+ timeoutMs?: number;
47
+ /**
48
+ * If true, waits for the domain to reach ready status after creation.
49
+ *
50
+ * Defaults to false (return immediately after create).
51
+ */
52
+ wait?: boolean;
53
+ /**
54
+ * Polling interval in milliseconds used when `wait` is true.
55
+ * Passed through to `waitForReady`.
56
+ */
57
+ waitIntervalMs?: number;
58
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { Domain } from '@heroku/types/3.sdk';
2
+ import type { ResourceCtx } from '../../../core/extend-resource.js';
3
+ import type { WaitForReadyOptions } from './types.js';
4
+ /**
5
+ * Waits for all pending domains on an app
6
+ * to reach ready status ('succeeded' or 'none').
7
+ *
8
+ * If `domain` or `hostname` is provided, waits for the specific domain.
9
+ *
10
+ * @throws if the domain reaches a non-ready status (e.g., 'failed').
11
+ */
12
+ export declare function waitForReady(ctx: Pick<ResourceCtx, 'platform'>, appIdentity: string, options?: WaitForReadyOptions): Promise<Domain[]>;
@@ -0,0 +1,46 @@
1
+ import { wait } from '../../../utils/wait.js';
2
+ const DEFAULT_WAIT_INTERVAL_MS = 5000;
3
+ /**
4
+ * Waits for all pending domains on an app
5
+ * to reach ready status ('succeeded' or 'none').
6
+ *
7
+ * If `domain` or `hostname` is provided, waits for the specific domain.
8
+ *
9
+ * @throws if the domain reaches a non-ready status (e.g., 'failed').
10
+ */
11
+ export async function waitForReady(ctx, appIdentity, options = {}) {
12
+ const { domain, hostname, signal, timeoutMs, waitIntervalMs = DEFAULT_WAIT_INTERVAL_MS } = options;
13
+ signal?.throwIfAborted();
14
+ const platform = signal ? ctx.platform.withOptions({ signal }) : ctx.platform;
15
+ let domains;
16
+ if (domain) {
17
+ domains = [domain];
18
+ }
19
+ else if (hostname) {
20
+ const domain = await platform.domain.info(appIdentity, hostname);
21
+ domains = [domain];
22
+ }
23
+ else {
24
+ const apiDomains = await platform.domain.list(appIdentity);
25
+ domains = apiDomains.filter(domain => domain.status === 'pending');
26
+ }
27
+ for (let i = 0; i < domains.length; i++) {
28
+ domains[i] = await waitForDomain(platform, appIdentity, domains[i], waitIntervalMs, timeoutMs, signal);
29
+ }
30
+ return domains;
31
+ }
32
+ async function waitForDomain(platform, appIdentity, domain, waitIntervalMs, timeoutMs, signal) {
33
+ const deadline = timeoutMs === undefined ? undefined : Date.now() + timeoutMs;
34
+ while (domain.status === 'pending') {
35
+ await wait(waitIntervalMs, signal);
36
+ const updatedDomain = await platform.domain.info(appIdentity, domain.id);
37
+ domain = updatedDomain;
38
+ if (deadline !== undefined && Date.now() >= deadline) {
39
+ throw new Error(`Timed out waiting for domain ${domain.hostname}`);
40
+ }
41
+ }
42
+ if (domain.status === 'succeeded' || domain.status === 'none') {
43
+ return domain;
44
+ }
45
+ throw new Error(`The domain creation finished with status ${domain.status}`);
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroku/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Heroku SDK",
5
5
  "author": "Heroku",
6
6
  "type": "module",