diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index 9e73bdb..8337378 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -16343,6 +16343,127 @@ } } }, + "/api/v1/status-pages/{id}/domains/{domainId}/primary": { + "post": { + "tags": [ + "Status Pages" + ], + "summary": "Mark a verified custom domain as the page's primary host", + "operationId": "setPrimaryDomain", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "domainId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseStatusPageCustomDomainDto" + } + } + } + }, + "400": { + "description": "Bad request — the payload failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized — missing or invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden — the actor lacks permission for this resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found — the requested resource does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict — the request collides with current resource state", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error — see the message field for details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "502": { + "description": "Bad gateway — an upstream provider returned an error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "503": { + "description": "Service unavailable — try again shortly", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, "/api/v1/status-pages/{id}/domains/{domainId}/verify": { "post": { "tags": [ @@ -29801,6 +29922,19 @@ "type": "string", "nullable": true }, + "cfCustomHostnameId": { + "type": "string", + "nullable": true + }, + "cfSslStatus": { + "type": "string", + "nullable": true + }, + "sslActiveAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, "createdAt": { "type": "string", "format": "date-time" diff --git a/src/commands/status-pages/domains/add.ts b/src/commands/status-pages/domains/add.ts index abf335b..021956d 100644 --- a/src/commands/status-pages/domains/add.ts +++ b/src/commands/status-pages/domains/add.ts @@ -1,11 +1,18 @@ import {Command, Flags} from '@oclif/core' +import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost, unwrapData} from '../../../lib/api-client.js' import {uuidArg} from '../../../lib/validators.js' +type StatusPageCustomDomain = components['schemas']['StatusPageCustomDomainDto'] + export default class StatusPagesDomainsAdd extends Command { - static description = 'Add a custom domain to a status page' - static examples = ['<%= config.bin %> status-pages domains add --hostname status.example.com'] + static description = 'Add a custom domain to a status page and print its verification record' + static examples = [ + '<%= config.bin %> status-pages domains add --hostname status.example.com', + '<%= config.bin %> status-pages domains add --hostname status.example.com --output json', + ] + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, @@ -16,6 +23,22 @@ export default class StatusPagesDomainsAdd extends Command { const {args, flags} = await this.parse(StatusPagesDomainsAdd) const client = buildClient(flags) const resp = await apiPost(client, `/api/v1/status-pages/${args.id}/domains`, {hostname: flags.hostname}) - display(this, unwrapData(resp), flags.output) + const domain = unwrapData(resp) as StatusPageCustomDomain + + if (flags.output === 'json' || flags.output === 'yaml') { + display(this, domain, flags.output) + return + } + + this.log(`Added '${domain.hostname}' (id ${domain.id}).`) + this.log('') + this.log('Verification record (create at your DNS provider):') + if (domain.verificationMethod === 'TXT') { + this.log(` TXT ${domain.hostname} → "${domain.verificationToken}"`) + } else { + this.log(` CNAME ${domain.hostname} → ${domain.verificationCnameTarget}`) + } + this.log('') + this.log(`Run 'devhelm status-pages domains verify ${args.id} ${domain.id}' once DNS has propagated.`) } } diff --git a/src/commands/status-pages/domains/list.ts b/src/commands/status-pages/domains/list.ts index 4b8c104..c8f5214 100644 --- a/src/commands/status-pages/domains/list.ts +++ b/src/commands/status-pages/domains/list.ts @@ -7,7 +7,7 @@ import {uuidArg} from '../../../lib/validators.js' type StatusPageCustomDomain = components['schemas']['StatusPageCustomDomainDto'] export default class StatusPagesDomainsList extends Command { - static description = 'List custom domains on a status page' + static description = 'List custom domains on a status page (incl. verification CNAME target)' static examples = ['<%= config.bin %> status-pages domains list '] static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = {...globalFlags} @@ -19,8 +19,11 @@ export default class StatusPagesDomainsList extends Command { display(this, items, flags.output, [ {header: 'ID', get: (r: StatusPageCustomDomain) => r.id ?? ''}, {header: 'HOSTNAME', get: (r: StatusPageCustomDomain) => r.hostname ?? ''}, - {header: 'VERIFIED', get: (r: StatusPageCustomDomain) => r.verifiedAt ?? ''}, + {header: 'PRIMARY', get: (r: StatusPageCustomDomain) => (r.primary ? 'yes' : '')}, {header: 'STATUS', get: (r: StatusPageCustomDomain) => r.status ?? ''}, + {header: 'SSL', get: (r: StatusPageCustomDomain) => r.cfSslStatus ?? ''}, + {header: 'CNAME TARGET', get: (r: StatusPageCustomDomain) => r.verificationCnameTarget ?? ''}, + {header: 'VERIFIED AT', get: (r: StatusPageCustomDomain) => r.verifiedAt ?? ''}, ]) } } diff --git a/src/commands/status-pages/domains/set-primary.ts b/src/commands/status-pages/domains/set-primary.ts new file mode 100644 index 0000000..ba323d8 --- /dev/null +++ b/src/commands/status-pages/domains/set-primary.ts @@ -0,0 +1,22 @@ +import {Command} from '@oclif/core' +import {globalFlags, buildClient, display} from '../../../lib/base-command.js' +import {apiPost, unwrapData} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' + +export default class StatusPagesDomainsSetPrimary extends Command { + static description = 'Mark a verified custom domain as the primary host for a status page' + static examples = ['<%= config.bin %> status-pages domains set-primary '] + static args = { + id: uuidArg({description: 'Status page ID', required: true}), + 'domain-id': uuidArg({description: 'Domain ID', required: true}), + } + + static flags = {...globalFlags} + + async run() { + const {args, flags} = await this.parse(StatusPagesDomainsSetPrimary) + const client = buildClient(flags) + const resp = await apiPost(client, `/api/v1/status-pages/${args.id}/domains/${args['domain-id']}/primary`, {}) + display(this, unwrapData(resp), flags.output) + } +} diff --git a/src/commands/status-pages/domains/verify.ts b/src/commands/status-pages/domains/verify.ts index 427b8d3..15a2d15 100644 --- a/src/commands/status-pages/domains/verify.ts +++ b/src/commands/status-pages/domains/verify.ts @@ -1,21 +1,42 @@ import {Command} from '@oclif/core' +import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost, unwrapData} from '../../../lib/api-client.js' import {uuidArg} from '../../../lib/validators.js' +type StatusPageCustomDomain = components['schemas']['StatusPageCustomDomainDto'] + export default class StatusPagesDomainsVerify extends Command { - static description = 'Verify a custom domain on a status page' + static description = 'Re-check verification + SSL status for a custom domain' static examples = ['<%= config.bin %> status-pages domains verify '] static args = { id: uuidArg({description: 'Status page ID', required: true}), 'domain-id': uuidArg({description: 'Domain ID', required: true}), } + static flags = {...globalFlags} async run() { const {args, flags} = await this.parse(StatusPagesDomainsVerify) const client = buildClient(flags) const resp = await apiPost(client, `/api/v1/status-pages/${args.id}/domains/${args['domain-id']}/verify`, {}) - display(this, unwrapData(resp), flags.output) + const domain = unwrapData(resp) as StatusPageCustomDomain + + if (flags.output === 'json' || flags.output === 'yaml') { + display(this, domain, flags.output) + return + } + + this.log(`Hostname: ${domain.hostname}`) + this.log(`Status: ${domain.status}`) + if (domain.cfSslStatus) this.log(`SSL: ${domain.cfSslStatus}`) + if (domain.verificationError) { + this.log('') + this.log(`Error: ${domain.verificationError}`) + this.log(`Expected CNAME → ${domain.verificationCnameTarget}`) + } else if (domain.status !== 'ACTIVE') { + this.log('') + this.log(`Expected CNAME → ${domain.verificationCnameTarget}`) + } } } diff --git a/src/lib/api-zod.generated.ts b/src/lib/api-zod.generated.ts index 00cb0e3..0b2719a 100644 --- a/src/lib/api-zod.generated.ts +++ b/src/lib/api-zod.generated.ts @@ -2693,6 +2693,9 @@ const StatusPageCustomDomainDto = z verificationCnameTarget: z.string(), verifiedAt: z.string().datetime({ offset: true }).nullish(), verificationError: z.string().nullish(), + cfCustomHostnameId: z.string().nullish(), + cfSslStatus: z.string().nullish(), + sslActiveAt: z.string().datetime({ offset: true }).nullish(), createdAt: z.string().datetime({ offset: true }), updatedAt: z.string().datetime({ offset: true }), primary: z.boolean(), diff --git a/src/lib/api.generated.ts b/src/lib/api.generated.ts index 693d519..ea4c9ab 100644 --- a/src/lib/api.generated.ts +++ b/src/lib/api.generated.ts @@ -1867,6 +1867,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/status-pages/{id}/domains/{domainId}/primary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Mark a verified custom domain as the page's primary host */ + post: operations["setPrimaryDomain"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/status-pages/{id}/domains/{domainId}/verify": { parameters: { query?: never; @@ -6155,6 +6172,10 @@ export interface components { /** Format: date-time */ verifiedAt?: string | null; verificationError?: string | null; + cfCustomHostnameId?: string | null; + cfSslStatus?: string | null; + /** Format: date-time */ + sslActiveAt?: string | null; /** Format: date-time */ createdAt: string; /** Format: date-time */ @@ -20609,6 +20630,101 @@ export interface operations { }; }; }; + setPrimaryDomain: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + domainId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["SingleValueResponseStatusPageCustomDomainDto"]; + }; + }; + /** @description Bad request — the payload failed validation */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Unauthorized — missing or invalid credentials */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Forbidden — the actor lacks permission for this resource */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Not found — the requested resource does not exist */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Conflict — the request collides with current resource state */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Internal server error — see the message field for details */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Bad gateway — an upstream provider returned an error */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Service unavailable — try again shortly */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; verifyDomain: { parameters: { query?: never;