From 59523ae83ed27b749775b5e3fed0c00c9adc6af7 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 13:21:57 +0200 Subject: [PATCH 01/10] refactor: use shared @devhelm/openapi-tools preprocessor Replace inline preprocessing functions in generate-zod.mjs with import from @devhelm/openapi-tools/preprocess. Adds missing flattenCircularOneOf step and fixes default-field skip logic. Made-with: Cursor --- package-lock.json | 22 ++++++++++ package.json | 1 + scripts/generate-zod.mjs | 80 +++--------------------------------- src/lib/api-zod.generated.ts | 25 +++++------ 4 files changed, 42 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46215e5..01cc057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "devhelm": "bin/run.js" }, "devDependencies": { + "@devhelm/openapi-tools": "file:../mini/packages/openapi-tools", "@types/lodash-es": "^4.17.12", "@types/node": "^25.5.2", "@typescript-eslint/eslint-plugin": "^8.58.1", @@ -38,6 +39,23 @@ "node": ">=18.0.0" } }, + "../mini/packages/openapi-tools": { + "name": "@devhelm/openapi-tools", + "version": "0.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "openapi-zod-client": "^1.18.0" + }, + "bin": { + "devhelm-openapi": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "20.16.11", + "openapi3-ts": "^4.5.0", + "typescript": "^5.7.0" + } + }, "node_modules/@apidevtools/json-schema-ref-parser": { "version": "11.7.2", "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", @@ -1349,6 +1367,10 @@ "node": ">=0.1.90" } }, + "node_modules/@devhelm/openapi-tools": { + "resolved": "../mini/packages/openapi-tools", + "link": true + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", diff --git a/package.json b/package.json index 5e3cd5c..f7abd76 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "zod": "^3.25.76" }, "devDependencies": { + "@devhelm/openapi-tools": "file:../mini/packages/openapi-tools", "@types/lodash-es": "^4.17.12", "@types/node": "^25.5.2", "@typescript-eslint/eslint-plugin": "^8.58.1", diff --git a/scripts/generate-zod.mjs b/scripts/generate-zod.mjs index fef05f0..45642d0 100644 --- a/scripts/generate-zod.mjs +++ b/scripts/generate-zod.mjs @@ -2,19 +2,17 @@ /** * Generate Zod schemas from the OpenAPI spec for CLI validation. * - * Applies the same Springdoc preprocessing as the dashboard's sync-schema, + * Uses @devhelm/openapi-tools for preprocessing (shared with all surfaces), * then runs openapi-zod-client to produce typed Zod schemas that the YAML * validation layer imports. * * Usage: node scripts/generate-zod.mjs - * - * Preprocessing logic is kept in sync with packages/openapi-tools in the - * monorepo. If you change preprocessing there, update it here too. */ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { preprocessSpec } from '@devhelm/openapi-tools/preprocess'; import { generateZodClientFromOpenAPI } from 'openapi-zod-client'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -22,71 +20,6 @@ const ROOT = join(__dirname, '..'); const SPEC_PATH = join(ROOT, 'docs/openapi/monitoring-api.json'); const OUTPUT_PATH = join(ROOT, 'src/lib/api-zod.generated.ts'); -// ── Springdoc preprocessing (synced from packages/openapi-tools) ────── - -function setRequiredFields(spec) { - const schemas = spec.components?.schemas ?? {}; - for (const schema of Object.values(schemas)) { - if (schema.type !== 'object' || !schema.properties) continue; - if (Array.isArray(schema.required)) { - for (const [prop, propSchema] of Object.entries(schema.properties)) { - if (propSchema.nullable) continue; - if (propSchema.oneOf && !schema.required.includes(prop)) { - schema.required.push(prop); - } - } - continue; - } - const required = []; - for (const [prop, propSchema] of Object.entries(schema.properties)) { - if (propSchema.nullable) continue; - if (propSchema.allOf) continue; - required.push(prop); - } - if (required.length > 0) schema.required = required; - } -} - -function setRequiredOnAllOfMembers(spec) { - const schemas = spec.components?.schemas ?? {}; - for (const schema of Object.values(schemas)) { - if (!Array.isArray(schema.allOf)) continue; - for (const member of schema.allOf) { - if (!member.properties) continue; - if (Array.isArray(member.required)) continue; - const required = []; - for (const [prop, propSchema] of Object.entries(member.properties)) { - if (propSchema.nullable) continue; - if (propSchema.allOf) continue; - required.push(prop); - } - if (required.length > 0) member.required = required; - } - } -} - -function pushRequiredIntoAllOf(spec) { - const schemas = spec.components?.schemas ?? {}; - for (const schema of Object.values(schemas)) { - if (!Array.isArray(schema.required) || !Array.isArray(schema.allOf)) continue; - for (const member of schema.allOf) { - if (!member.properties) continue; - const memberRequired = []; - for (const field of schema.required) { - if (field in member.properties) memberRequired.push(field); - } - if (memberRequired.length > 0) { - member.required = member.required - ? [...new Set([...member.required, ...memberRequired])] - : memberRequired; - } - } - } -} - -// ── Post-processing (strip Zodios client, keep only Zod schemas) ───── -// Same approach as sdk-js/scripts/generate-schemas.js - function extractSchemas(raw) { const lines = raw.split('\n'); const kept = []; @@ -99,16 +32,15 @@ function extractSchemas(raw) { kept.join('\n') + '\n'; } -// ── Main ────────────────────────────────────────────────────────────── - async function main() { console.log('Reading spec from', SPEC_PATH); const spec = JSON.parse(readFileSync(SPEC_PATH, 'utf8')); - setRequiredFields(spec); - setRequiredOnAllOfMembers(spec); - pushRequiredIntoAllOf(spec); + const { flattened } = preprocessSpec(spec); console.log(`Preprocessed spec (${Object.keys(spec.components?.schemas ?? {}).length} schemas)`); + if (flattened.length > 0) { + console.log(` Flattened circular oneOf: ${flattened.join(', ')}`); + } mkdirSync(dirname(OUTPUT_PATH), { recursive: true }); diff --git a/src/lib/api-zod.generated.ts b/src/lib/api-zod.generated.ts index 97ff5f9..75278d9 100644 --- a/src/lib/api-zod.generated.ts +++ b/src/lib/api-zod.generated.ts @@ -890,55 +890,56 @@ const StatusPageBranding = z .min(0) .max(2048) .regex(/^https?:\/\/.*/) - .nullish(), + .nullable(), faviconUrl: z .string() .min(0) .max(2048) .regex(/^https?:\/\/.*/) - .nullish(), + .nullable(), brandColor: z .string() .min(0) .max(30) .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/) - .nullish(), + .nullable(), pageBackground: z .string() .min(0) .max(30) .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/) - .nullish(), + .nullable(), cardBackground: z .string() .min(0) .max(30) .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/) - .nullish(), + .nullable(), textColor: z .string() .min(0) .max(30) .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/) - .nullish(), + .nullable(), borderColor: z .string() .min(0) .max(30) .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/) - .nullish(), - headerStyle: z.string().min(0).max(50).nullish(), - theme: z.string().min(0).max(50).nullish(), + .nullable(), + headerStyle: z.string().min(0).max(50).nullable(), + theme: z.string().min(0).max(50).nullable(), reportUrl: z .string() .min(0) .max(2048) .regex(/^https?:\/\/.*/) - .nullish(), + .nullable(), hidePoweredBy: z.boolean().default(false), - customCss: z.string().min(0).max(50000).nullish(), - customHeadHtml: z.string().min(0).max(50000).nullish(), + customCss: z.string().min(0).max(50000).nullable(), + customHeadHtml: z.string().min(0).max(50000).nullable(), }) + .partial() .passthrough(); const CreateStatusPageRequest = z .object({ From b03b3d507caec97be67dcf662f791a76f0a2b761 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 14:09:44 +0200 Subject: [PATCH 02/10] feat: auto-generate enum constants from OpenAPI spec and add field parity tests Replace 14 hand-maintained enum arrays in zod-schemas.ts and schema.ts with imports from spec-facts.generated.ts, which is extracted from the OpenAPI spec during `npm run zodgen`. Add spec-field-parity.test.ts (39 tests) that verify every YAML field maps to an API request DTO field and vice versa, catching schema drift at compile time. Made-with: Cursor --- scripts/generate-zod.mjs | 67 ++++++++ src/lib/api-zod.generated.ts | 14 +- src/lib/spec-facts.generated.ts | 51 ++++++ src/lib/yaml/schema.ts | 37 +++-- src/lib/yaml/validator.ts | 4 +- src/lib/yaml/zod-schemas.ts | 34 ++-- test/yaml/spec-field-parity.test.ts | 233 ++++++++++++++++++++++++++++ 7 files changed, 396 insertions(+), 44 deletions(-) create mode 100644 src/lib/spec-facts.generated.ts create mode 100644 test/yaml/spec-field-parity.test.ts diff --git a/scripts/generate-zod.mjs b/scripts/generate-zod.mjs index 45642d0..32e65f6 100644 --- a/scripts/generate-zod.mjs +++ b/scripts/generate-zod.mjs @@ -32,6 +32,71 @@ function extractSchemas(raw) { kept.join('\n') + '\n'; } +const FACTS_PATH = join(ROOT, 'src/lib/spec-facts.generated.ts'); + +/** + * Extract enum values and constraints from the OpenAPI spec to produce + * a spec-facts file. These constants replace hand-maintained arrays in + * zod-schemas.ts and schema.ts — if the API adds a new enum value or + * changes a constraint, re-running zodgen picks it up automatically. + */ +function generateSpecFacts(spec) { + const schemas = spec.components?.schemas ?? {}; + + function enumsFrom(schemaName, propName) { + const s = schemas[schemaName]; + if (!s) return null; + if (s.properties?.[propName]?.enum) return s.properties[propName].enum; + if (s.allOf) { + for (const member of s.allOf) { + if (member.properties?.[propName]?.enum) return member.properties[propName].enum; + if (member.properties?.[propName]?.items?.enum) return member.properties[propName].items.enum; + } + } + return null; + } + + const facts = { + MONITOR_TYPES: enumsFrom('CreateMonitorRequest', 'type'), + HTTP_METHODS: enumsFrom('HttpMonitorConfig', 'method'), + DNS_RECORD_TYPES: enumsFrom('DnsMonitorConfig', 'recordTypes'), + ASSERTION_SEVERITIES: enumsFrom('CreateAssertionRequest', 'severity'), + CHANNEL_TYPES: enumsFrom('AlertChannelDto', 'channelType'), + TRIGGER_RULE_TYPES: enumsFrom('TriggerRule', 'type'), + TRIGGER_SCOPES: enumsFrom('TriggerRule', 'scope'), + TRIGGER_SEVERITIES: enumsFrom('TriggerRule', 'severity'), + TRIGGER_AGGREGATIONS: enumsFrom('TriggerRule', 'aggregationType'), + ALERT_SENSITIVITIES: enumsFrom('ServiceSubscriptionDto', 'alertSensitivity'), + HEALTH_THRESHOLD_TYPES: enumsFrom('CreateResourceGroupRequest', 'healthThresholdType'), + STATUS_PAGE_VISIBILITIES: enumsFrom('CreateStatusPageRequest', 'visibility'), + STATUS_PAGE_INCIDENT_MODES: enumsFrom('CreateStatusPageRequest', 'incidentMode'), + STATUS_PAGE_COMPONENT_TYPES: enumsFrom('CreateStatusPageComponentRequest', 'type'), + AUTH_TYPES: enumsFrom('MonitorAuthDto', 'authType'), + MANAGED_BY: enumsFrom('CreateMonitorRequest', 'managedBy'), + }; + + const lines = [ + '// Auto-generated from OpenAPI spec. DO NOT EDIT.', + '// Re-run `npm run zodgen` to regenerate.', + '', + ]; + + for (const [name, values] of Object.entries(facts)) { + if (!values) { + lines.push(`// WARNING: ${name} — enum not found in spec`); + continue; + } + const items = values.map(v => `'${v}'`).join(', '); + lines.push(`export const ${name} = [${items}] as const`); + const typeName = name.split('_').map(w => w[0] + w.slice(1).toLowerCase()).join(''); + lines.push(`export type ${typeName} = (typeof ${name})[number]`); + lines.push(''); + } + + writeFileSync(FACTS_PATH, lines.join('\n') + '\n', 'utf8'); + console.log(`Generated spec facts (${Object.keys(facts).length} enums) → ${FACTS_PATH}`); +} + async function main() { console.log('Reading spec from', SPEC_PATH); const spec = JSON.parse(readFileSync(SPEC_PATH, 'utf8')); @@ -42,6 +107,8 @@ async function main() { console.log(` Flattened circular oneOf: ${flattened.join(', ')}`); } + generateSpecFacts(spec); + mkdirSync(dirname(OUTPUT_PATH), { recursive: true }); await generateZodClientFromOpenAPI({ diff --git a/src/lib/api-zod.generated.ts b/src/lib/api-zod.generated.ts index 75278d9..8c22ff0 100644 --- a/src/lib/api-zod.generated.ts +++ b/src/lib/api-zod.generated.ts @@ -119,7 +119,7 @@ const CreateEnvironmentRequest = z .max(100) .regex(/^[a-z0-9][a-z0-9_-]*$/), variables: z.record(z.string().nullable()).nullish(), - isDefault: z.boolean().optional(), + isDefault: z.boolean(), }) .passthrough(); const UpdateEnvironmentRequest = z @@ -536,8 +536,8 @@ const TriggerRule = z const ConfirmationPolicy = z .object({ type: z.literal("multi_region"), - minRegionsFailing: z.number().int().optional(), - maxWaitSeconds: z.number().int().optional(), + minRegionsFailing: z.number().int(), + maxWaitSeconds: z.number().int(), }) .passthrough(); const RecoveryPolicy = z @@ -755,7 +755,7 @@ const MatchRule = z .passthrough(); const EscalationStep = z .object({ - delayMinutes: z.number().int().gte(0).optional(), + delayMinutes: z.number().int().gte(0), channelIds: z.array(z.string().uuid()).min(1), requireAck: z.boolean().nullish(), repeatIntervalSeconds: z.number().int().gte(1).nullish(), @@ -812,8 +812,8 @@ const UpdateOrgDetailsRequest = z const RetryStrategy = z .object({ type: z.string(), - maxRetries: z.number().int().optional(), - interval: z.number().int().optional(), + maxRetries: z.number().int(), + interval: z.number().int(), }) .passthrough(); const CreateResourceGroupRequest = z @@ -997,7 +997,7 @@ const UpdateStatusPageComponentRequest = z const ComponentPosition = z .object({ componentId: z.string().uuid(), - displayOrder: z.number().int().optional(), + displayOrder: z.number().int(), groupId: z.string().uuid().nullish(), }) .passthrough(); diff --git a/src/lib/spec-facts.generated.ts b/src/lib/spec-facts.generated.ts new file mode 100644 index 0000000..3e1b003 --- /dev/null +++ b/src/lib/spec-facts.generated.ts @@ -0,0 +1,51 @@ +// Auto-generated from OpenAPI spec. DO NOT EDIT. +// Re-run `npm run zodgen` to regenerate. + +export const MONITOR_TYPES = ['HTTP', 'DNS', 'MCP_SERVER', 'TCP', 'ICMP', 'HEARTBEAT'] as const +export type MonitorTypes = (typeof MONITOR_TYPES)[number] + +export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const +export type HttpMethods = (typeof HTTP_METHODS)[number] + +export const DNS_RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV', 'SOA', 'CAA', 'PTR'] as const +export type DnsRecordTypes = (typeof DNS_RECORD_TYPES)[number] + +export const ASSERTION_SEVERITIES = ['fail', 'warn'] as const +export type AssertionSeverities = (typeof ASSERTION_SEVERITIES)[number] + +export const CHANNEL_TYPES = ['email', 'webhook', 'slack', 'pagerduty', 'opsgenie', 'teams', 'discord'] as const +export type ChannelTypes = (typeof CHANNEL_TYPES)[number] + +export const TRIGGER_RULE_TYPES = ['consecutive_failures', 'failures_in_window', 'response_time'] as const +export type TriggerRuleTypes = (typeof TRIGGER_RULE_TYPES)[number] + +export const TRIGGER_SCOPES = ['per_region', 'any_region'] as const +export type TriggerScopes = (typeof TRIGGER_SCOPES)[number] + +export const TRIGGER_SEVERITIES = ['down', 'degraded'] as const +export type TriggerSeverities = (typeof TRIGGER_SEVERITIES)[number] + +export const TRIGGER_AGGREGATIONS = ['all_exceed', 'average', 'p95', 'max'] as const +export type TriggerAggregations = (typeof TRIGGER_AGGREGATIONS)[number] + +export const ALERT_SENSITIVITIES = ['ALL', 'INCIDENTS_ONLY', 'MAJOR_ONLY'] as const +export type AlertSensitivities = (typeof ALERT_SENSITIVITIES)[number] + +export const HEALTH_THRESHOLD_TYPES = ['COUNT', 'PERCENTAGE'] as const +export type HealthThresholdTypes = (typeof HEALTH_THRESHOLD_TYPES)[number] + +export const STATUS_PAGE_VISIBILITIES = ['PUBLIC', 'PASSWORD', 'IP_RESTRICTED'] as const +export type StatusPageVisibilities = (typeof STATUS_PAGE_VISIBILITIES)[number] + +export const STATUS_PAGE_INCIDENT_MODES = ['MANUAL', 'REVIEW', 'AUTOMATIC'] as const +export type StatusPageIncidentModes = (typeof STATUS_PAGE_INCIDENT_MODES)[number] + +export const STATUS_PAGE_COMPONENT_TYPES = ['MONITOR', 'GROUP', 'STATIC'] as const +export type StatusPageComponentTypes = (typeof STATUS_PAGE_COMPONENT_TYPES)[number] + +export const AUTH_TYPES = ['bearer', 'basic', 'header', 'api_key'] as const +export type AuthTypes = (typeof AUTH_TYPES)[number] + +export const MANAGED_BY = ['DASHBOARD', 'CLI', 'TERRAFORM'] as const +export type ManagedBy = (typeof MANAGED_BY)[number] + diff --git a/src/lib/yaml/schema.ts b/src/lib/yaml/schema.ts index b6fa483..84534b3 100644 --- a/src/lib/yaml/schema.ts +++ b/src/lib/yaml/schema.ts @@ -9,6 +9,12 @@ * anti-drift default injection. */ import type {components} from '../api.generated.js' +import { + MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, ASSERTION_SEVERITIES, + CHANNEL_TYPES, TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, + TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, + STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES, +} from '../spec-facts.generated.js' type Schemas = components['schemas'] @@ -26,21 +32,18 @@ export type ComparisonOperator = Schemas['StatusCodeAssertion'] extends {type: s ? R extends {operator: infer O} ? O : never : never -// ── Enum constants for validation ────────────────────────────────────── +// ── Re-export generated enum constants ──────────────────────────────── +// These are auto-extracted from the OpenAPI spec via spec-facts.generated.ts. +// Previously maintained by hand — now they update automatically on `npm run zodgen`. -export const MONITOR_TYPES: readonly MonitorType[] = ['HTTP', 'DNS', 'TCP', 'ICMP', 'HEARTBEAT', 'MCP_SERVER'] -export const HTTP_METHODS: readonly HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] -export const DNS_RECORD_TYPES: readonly string[] = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV', 'SOA', 'CAA', 'PTR'] -export const ASSERTION_SEVERITIES: readonly AssertionSeverity[] = ['fail', 'warn'] -export const COMPARISON_OPERATORS: readonly string[] = ['equals', 'contains', 'less_than', 'greater_than', 'matches', 'range'] -export const TRIGGER_RULE_TYPES: readonly TriggerRuleType[] = ['consecutive_failures', 'failures_in_window', 'response_time'] -export const TRIGGER_SCOPES: readonly string[] = ['per_region', 'any_region'] -export const TRIGGER_SEVERITIES: readonly TriggerRuleSeverity[] = ['down', 'degraded'] -export const TRIGGER_AGGREGATIONS: readonly string[] = ['all_exceed', 'average', 'p95', 'max'] -export const CHANNEL_TYPES = ['slack', 'email', 'pagerduty', 'opsgenie', 'discord', 'teams', 'webhook'] as const +export { + MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, ASSERTION_SEVERITIES, + CHANNEL_TYPES, TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, + TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, +} export type ChannelType = (typeof CHANNEL_TYPES)[number] -export const ALERT_SENSITIVITIES = ['ALL', 'INCIDENTS_ONLY', 'MAJOR_ONLY'] as const -export const HEALTH_THRESHOLD_TYPES = ['COUNT', 'PERCENTAGE'] as const + +export const COMPARISON_OPERATORS: readonly string[] = ['equals', 'contains', 'less_than', 'greater_than', 'matches', 'range'] export const MIN_FREQUENCY = 30 export const MAX_FREQUENCY = 86400 @@ -351,17 +354,17 @@ export interface YamlDependency { // ── Status Page types ────────────────────────────────────────────────── +export {STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES} + // Note: the API's SpVisibility enum also declares PASSWORD and IP_RESTRICTED, // but those modes are not yet wired to storage or enforcement server-side. // YAML/CLI deliberately only accepts PUBLIC until the API implements them // so users cannot set a value that silently has no effect. export type StatusPageVisibility = 'PUBLIC' -export type StatusPageIncidentMode = 'MANUAL' | 'REVIEW' | 'AUTOMATIC' -export type StatusPageComponentType = 'MONITOR' | 'GROUP' | 'STATIC' +export type StatusPageIncidentMode = (typeof STATUS_PAGE_INCIDENT_MODES)[number] +export type StatusPageComponentType = (typeof STATUS_PAGE_COMPONENT_TYPES)[number] export const STATUS_PAGE_VISIBILITIES: readonly StatusPageVisibility[] = ['PUBLIC'] -export const STATUS_PAGE_INCIDENT_MODES: readonly StatusPageIncidentMode[] = ['MANUAL', 'REVIEW', 'AUTOMATIC'] -export const STATUS_PAGE_COMPONENT_TYPES: readonly StatusPageComponentType[] = ['MONITOR', 'GROUP', 'STATIC'] /** * Visual tokens applied to the public status page. Every field is optional; diff --git a/src/lib/yaml/validator.ts b/src/lib/yaml/validator.ts index 98de907..0f09d80 100644 --- a/src/lib/yaml/validator.ts +++ b/src/lib/yaml/validator.ts @@ -387,7 +387,7 @@ function validateMonitorConfig(type: string, config: YamlMonitorConfig, path: st if (!('hostname' in config) || !config.hostname) ctx.error(`${path}.hostname`, 'DNS monitor requires "hostname"') if ('recordTypes' in config && config.recordTypes && Array.isArray(config.recordTypes)) { for (const rt of config.recordTypes) { - if (!DNS_RECORD_TYPES.includes(rt as string)) { + if (!(DNS_RECORD_TYPES as readonly string[]).includes(rt as string)) { ctx.error(`${path}.recordTypes`, `Invalid DNS record type "${rt}". Must be one of: ${DNS_RECORD_TYPES.join(', ')}`) } } @@ -479,7 +479,7 @@ function validateIncidentPolicy(policy: YamlIncidentPolicy, path: string, ctx: V if (!TRIGGER_SEVERITIES.includes(rule.severity)) { ctx.error(`${rpath}.severity`, `Must be one of: ${TRIGGER_SEVERITIES.join(', ')}`) } - if (rule.aggregationType && !TRIGGER_AGGREGATIONS.includes(rule.aggregationType as string)) { + if (rule.aggregationType && !(TRIGGER_AGGREGATIONS as readonly string[]).includes(rule.aggregationType as string)) { ctx.error(`${rpath}.aggregationType`, `Must be one of: ${TRIGGER_AGGREGATIONS.join(', ')}`) } } diff --git a/src/lib/yaml/zod-schemas.ts b/src/lib/yaml/zod-schemas.ts index a580f96..3c583bf 100644 --- a/src/lib/yaml/zod-schemas.ts +++ b/src/lib/yaml/zod-schemas.ts @@ -8,9 +8,10 @@ * it only composes generated schemas into dispatch maps and top-level * structural schemas. * - * Enum constants are re-declared as `as const` tuples because Zod's - * `z.enum` requires a literal tuple type. The parity test in - * `zod-schemas.test.ts` asserts they stay in sync with schema.ts. + * Enum constants are imported from spec-facts.generated.ts (auto-extracted + * from the OpenAPI spec). The parity test in zod-schemas.test.ts asserts + * they stay in sync with schema.ts. The spec-field-parity test verifies + * every YAML field maps to a real API request field. * * Auth schemas remain hand-written because the YAML format uses a * `secret` field that doesn't exist in the API (the CLI resolves it @@ -19,6 +20,12 @@ import {z} from 'zod' import {schemas as apiSchemas} from '../api-zod.generated.js' +import { + MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, ASSERTION_SEVERITIES, + CHANNEL_TYPES, TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, + TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, + STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES, +} from '../spec-facts.generated.js' // ── Assertion config schemas (imported from generated OpenAPI Zod) ──── // Maps wire-format type strings (from AssertionConfig discriminator) @@ -96,22 +103,13 @@ const MONITOR_TYPE_CONFIG_SCHEMAS: Record = { MCP_SERVER: apiSchemas.McpServerMonitorConfig, } -// ── Constants (kept in sync with schema.ts via parity test) ────────── - -const MONITOR_TYPES = ['HTTP', 'DNS', 'TCP', 'ICMP', 'HEARTBEAT', 'MCP_SERVER'] as const -const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const -const DNS_RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV', 'SOA', 'CAA', 'PTR'] as const -const ASSERTION_SEVERITIES = ['fail', 'warn'] as const -const CHANNEL_TYPES = ['slack', 'email', 'pagerduty', 'opsgenie', 'discord', 'teams', 'webhook'] as const -const TRIGGER_RULE_TYPES = ['consecutive_failures', 'failures_in_window', 'response_time'] as const -const TRIGGER_SCOPES = ['per_region', 'any_region'] as const -const TRIGGER_SEVERITIES = ['down', 'degraded'] as const -const TRIGGER_AGGREGATIONS = ['all_exceed', 'average', 'p95', 'max'] as const -const ALERT_SENSITIVITIES = ['ALL', 'INCIDENTS_ONLY', 'MAJOR_ONLY'] as const -const HEALTH_THRESHOLD_TYPES = ['COUNT', 'PERCENTAGE'] as const +// ── Constants ──────────────────────────────────────────────────────── +// Enum tuples are imported from spec-facts.generated.ts (auto-extracted +// from the OpenAPI spec). Only STATUS_PAGE_VISIBILITIES is intentionally +// narrowed — the API also accepts PASSWORD and IP_RESTRICTED, but those +// modes are not yet wired to storage or enforcement server-side. + const STATUS_PAGE_VISIBILITIES = ['PUBLIC'] as const -const STATUS_PAGE_INCIDENT_MODES = ['MANUAL', 'REVIEW', 'AUTOMATIC'] as const -const STATUS_PAGE_COMPONENT_TYPES = ['MONITOR', 'GROUP', 'STATIC'] as const const MIN_FREQUENCY = 30 const MAX_FREQUENCY = 86400 diff --git a/test/yaml/spec-field-parity.test.ts b/test/yaml/spec-field-parity.test.ts new file mode 100644 index 0000000..c129a4f --- /dev/null +++ b/test/yaml/spec-field-parity.test.ts @@ -0,0 +1,233 @@ +/** + * Compile-time parity test: verifies that every field defined in the + * hand-authored Zod schemas (zod-schemas.ts) maps to a real field in + * the corresponding OpenAPI request DTO — unless it's a known YAML-only + * field (name references that the transform layer resolves to UUIDs). + * + * Also checks the reverse: that every API request field is either + * present in the YAML schema or listed as API-only (e.g. managedBy, + * UUID fields that YAML doesn't expose). + * + * This catches two classes of bugs: + * 1. YAML schema has a field that was removed from the API → stale field + * 2. API added a new field that YAML doesn't know about → missing field + */ +import {describe, it, expect} from 'vitest' +import {readFileSync} from 'node:fs' +import {join, dirname} from 'node:path' +import {fileURLToPath} from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const ROOT = join(__dirname, '..', '..') +const spec = JSON.parse(readFileSync(join(ROOT, 'docs/openapi/monitoring-api.json'), 'utf8')) + +function specFields(...schemaNames: string[]): string[] { + const props = new Set() + for (const schemaName of schemaNames) { + const s = spec.components?.schemas?.[schemaName] + if (!s) continue + if (s.properties) for (const k of Object.keys(s.properties)) props.add(k) + if (s.allOf) { + for (const member of s.allOf) { + if (member.properties) for (const k of Object.keys(member.properties)) props.add(k) + } + } + } + return [...props] +} + +// YAML fields that exist only in YAML (name/slug references resolved by transform layer) +const YAML_ONLY_FIELDS: Record = { + monitor: ['environment', 'tags', 'alertChannels'], + alertChannel: [], + notificationPolicy: [], + resourceGroup: [ + 'alertPolicy', 'defaultAlertChannels', 'defaultEnvironment', + 'monitors', 'services', + ], + statusPage: ['componentGroups', 'components'], + statusPageComponent: ['monitor', 'resourceGroup', 'group'], + webhook: [], + tag: [], + environment: [], + secret: [], + triggerRule: [], + confirmationPolicy: [], + recoveryPolicy: [], + dependency: ['service', 'alertSensitivity', 'component'], +} + +// API fields that YAML intentionally does not expose (set by CLI internally, or UUID-only) +const API_ONLY_FIELDS: Record = { + monitor: ['managedBy', 'environmentId', 'alertChannelIds', 'clearEnvironmentId', 'clearAuth'], + alertChannel: [], + notificationPolicy: [], + resourceGroup: [ + 'alertPolicyId', 'defaultEnvironmentId', + ], + statusPage: [], + statusPageComponent: ['monitorId', 'resourceGroupId', 'groupId', 'displayOrder', 'removeFromGroup'], + webhook: ['enabled'], + tag: [], + environment: [], + secret: [], + triggerRule: [], + confirmationPolicy: [], + recoveryPolicy: [], + dependency: [], +} + +interface FieldMapping { + yamlName: string + apiSchemaNames: string[] + yamlFields: string[] + yamlOnlyFields: string[] + apiOnlyFields: string[] +} + +const MAPPINGS: FieldMapping[] = [ + { + yamlName: 'monitor', + apiSchemaNames: ['CreateMonitorRequest', 'UpdateMonitorRequest'], + yamlFields: [ + 'name', 'type', 'config', 'frequencySeconds', 'enabled', 'regions', + 'environment', 'tags', 'alertChannels', 'assertions', 'auth', 'incidentPolicy', + ], + yamlOnlyFields: YAML_ONLY_FIELDS['monitor'], + apiOnlyFields: API_ONLY_FIELDS['monitor'], + }, + { + yamlName: 'alertChannel', + apiSchemaNames: ['CreateAlertChannelRequest', 'UpdateAlertChannelRequest'], + yamlFields: ['name', 'config'], + yamlOnlyFields: YAML_ONLY_FIELDS['alertChannel'], + apiOnlyFields: API_ONLY_FIELDS['alertChannel'], + }, + { + yamlName: 'notificationPolicy', + apiSchemaNames: ['CreateNotificationPolicyRequest', 'UpdateNotificationPolicyRequest'], + yamlFields: ['name', 'enabled', 'priority', 'matchRules', 'escalation'], + yamlOnlyFields: YAML_ONLY_FIELDS['notificationPolicy'], + apiOnlyFields: API_ONLY_FIELDS['notificationPolicy'], + }, + { + yamlName: 'resourceGroup', + apiSchemaNames: ['CreateResourceGroupRequest', 'UpdateResourceGroupRequest'], + yamlFields: [ + 'name', 'description', 'alertPolicy', 'defaultFrequency', 'defaultRegions', + 'defaultRetryStrategy', 'defaultAlertChannels', 'defaultEnvironment', + 'healthThresholdType', 'healthThresholdValue', 'suppressMemberAlerts', + 'confirmationDelaySeconds', 'recoveryCooldownMinutes', 'monitors', 'services', + ], + yamlOnlyFields: YAML_ONLY_FIELDS['resourceGroup'], + apiOnlyFields: API_ONLY_FIELDS['resourceGroup'], + }, + { + yamlName: 'statusPage', + apiSchemaNames: ['CreateStatusPageRequest', 'UpdateStatusPageRequest'], + yamlFields: [ + 'name', 'slug', 'description', 'visibility', 'enabled', + 'incidentMode', 'branding', 'componentGroups', 'components', + ], + yamlOnlyFields: YAML_ONLY_FIELDS['statusPage'], + apiOnlyFields: API_ONLY_FIELDS['statusPage'], + }, + { + yamlName: 'statusPageComponent', + apiSchemaNames: ['CreateStatusPageComponentRequest', 'UpdateStatusPageComponentRequest'], + yamlFields: [ + 'name', 'description', 'type', 'monitor', 'resourceGroup', + 'group', 'showUptime', 'excludeFromOverall', 'startDate', + ], + yamlOnlyFields: YAML_ONLY_FIELDS['statusPageComponent'], + apiOnlyFields: API_ONLY_FIELDS['statusPageComponent'], + }, + { + yamlName: 'webhook', + apiSchemaNames: ['CreateWebhookEndpointRequest', 'UpdateWebhookEndpointRequest'], + yamlFields: ['url', 'subscribedEvents', 'description', 'enabled'], + yamlOnlyFields: YAML_ONLY_FIELDS['webhook'], + apiOnlyFields: API_ONLY_FIELDS['webhook'], + }, + { + yamlName: 'tag', + apiSchemaNames: ['CreateTagRequest'], + yamlFields: ['name', 'color'], + yamlOnlyFields: YAML_ONLY_FIELDS['tag'], + apiOnlyFields: API_ONLY_FIELDS['tag'], + }, + { + yamlName: 'environment', + apiSchemaNames: ['CreateEnvironmentRequest', 'UpdateEnvironmentRequest'], + yamlFields: ['name', 'slug', 'variables', 'isDefault'], + yamlOnlyFields: YAML_ONLY_FIELDS['environment'], + apiOnlyFields: API_ONLY_FIELDS['environment'], + }, + { + yamlName: 'secret', + apiSchemaNames: ['CreateSecretRequest'], + yamlFields: ['key', 'value'], + yamlOnlyFields: YAML_ONLY_FIELDS['secret'], + apiOnlyFields: API_ONLY_FIELDS['secret'], + }, + { + yamlName: 'triggerRule', + apiSchemaNames: ['TriggerRule'], + yamlFields: [ + 'type', 'count', 'windowMinutes', 'scope', + 'thresholdMs', 'severity', 'aggregationType', + ], + yamlOnlyFields: YAML_ONLY_FIELDS['triggerRule'], + apiOnlyFields: API_ONLY_FIELDS['triggerRule'], + }, + { + yamlName: 'confirmationPolicy', + apiSchemaNames: ['ConfirmationPolicy'], + yamlFields: ['type', 'minRegionsFailing', 'maxWaitSeconds'], + yamlOnlyFields: YAML_ONLY_FIELDS['confirmationPolicy'], + apiOnlyFields: API_ONLY_FIELDS['confirmationPolicy'], + }, + { + yamlName: 'recoveryPolicy', + apiSchemaNames: ['RecoveryPolicy'], + yamlFields: ['consecutiveSuccesses', 'minRegionsPassing', 'cooldownMinutes'], + yamlOnlyFields: YAML_ONLY_FIELDS['recoveryPolicy'], + apiOnlyFields: API_ONLY_FIELDS['recoveryPolicy'], + }, +] + +describe('YAML ↔ OpenAPI field parity', () => { + for (const mapping of MAPPINGS) { + describe(mapping.yamlName, () => { + const apiFields = specFields(...mapping.apiSchemaNames) + const label = mapping.apiSchemaNames.join(' + ') + + it(`API schema(s) [${label}] exist in the OpenAPI spec`, () => { + expect(apiFields.length).toBeGreaterThan(0) + }) + + it('every YAML field maps to an API field or is YAML-only', () => { + for (const field of mapping.yamlFields) { + if (mapping.yamlOnlyFields.includes(field)) continue + expect( + apiFields, + `YAML field "${field}" on ${mapping.yamlName} not found in API schemas [${label}]. ` + + `If this is intentionally YAML-only, add it to YAML_ONLY_FIELDS['${mapping.yamlName}'].`, + ).toContain(field) + } + }) + + it('every API field is covered by YAML or listed as API-only', () => { + for (const field of apiFields) { + const inYaml = mapping.yamlFields.includes(field) + const isApiOnly = mapping.apiOnlyFields.includes(field) + expect( + inYaml || isApiOnly, + `API field "${field}" on [${label}] is not in the YAML schema and not in API_ONLY_FIELDS['${mapping.yamlName}']. ` + + `Either add it to the YAML schema or mark it as API-only.`, + ).toBe(true) + } + }) + }) + } +}) From aea6aa3dcf450e033029fe129dea339d652a7696 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 14:26:08 +0200 Subject: [PATCH 03/10] test: extend field parity tests to snapshot functions and nested configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 9 new parity tests (39→48) covering: - statusPageComponentDesiredSnapshot fields vs StatusPageComponentDto - statusPageGroupDesiredSnapshot fields vs StatusPageComponentGroupDto - StatusPageBranding bidirectional field check - EscalationStep fields (channels↔channelIds mapping) - MatchRule fields (monitorNames↔monitorIds mapping) Ensures API schema additions are caught at compile time before they cause silent drift in the reconciliation loop. Made-with: Cursor --- test/yaml/spec-field-parity.test.ts | 171 ++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/test/yaml/spec-field-parity.test.ts b/test/yaml/spec-field-parity.test.ts index c129a4f..32906b5 100644 --- a/test/yaml/spec-field-parity.test.ts +++ b/test/yaml/spec-field-parity.test.ts @@ -231,3 +231,174 @@ describe('YAML ↔ OpenAPI field parity', () => { }) } }) + +// ── Snapshot function parity ────────────────────────────────────────── +// Verifies that the field sets in statusPageComponentDesiredSnapshot and +// statusPageGroupDesiredSnapshot stay aligned with the corresponding +// response DTO schemas. If the API adds a field to StatusPageComponentDto, +// the snapshot must be updated or the drift comparison will silently ignore it. + +const SNAPSHOT_COMPONENT_FIELDS = [ + 'name', 'description', 'type', 'showUptime', 'excludeFromOverall', + 'startDate', 'group', 'monitor', 'resourceGroup', +] + +const SNAPSHOT_COMPONENT_DTO_ONLY = [ + 'id', 'statusPageId', 'groupId', 'monitorId', 'resourceGroupId', + 'displayOrder', 'pageOrder', 'currentStatus', 'createdAt', 'updatedAt', +] + +const SNAPSHOT_GROUP_FIELDS = ['name', 'description', 'collapsed'] + +const SNAPSHOT_GROUP_DTO_ONLY = [ + 'id', 'statusPageId', 'displayOrder', 'pageOrder', + 'components', 'createdAt', 'updatedAt', +] + +describe('Snapshot ↔ DTO field parity', () => { + describe('statusPageComponentDesiredSnapshot', () => { + const dtoFields = specFields('StatusPageComponentDto') + + it('StatusPageComponentDto exists in the spec', () => { + expect(dtoFields.length).toBeGreaterThan(0) + }) + + it('every snapshot field maps to a DTO field or is a YAML name ref', () => { + const yamlNameRefs = ['group', 'monitor', 'resourceGroup'] + for (const field of SNAPSHOT_COMPONENT_FIELDS) { + if (yamlNameRefs.includes(field)) continue + expect( + dtoFields, + `Snapshot field "${field}" not found in StatusPageComponentDto`, + ).toContain(field) + } + }) + + it('every DTO field is in the snapshot or listed as DTO-only', () => { + for (const field of dtoFields) { + const inSnapshot = SNAPSHOT_COMPONENT_FIELDS.includes(field) + const isDtoOnly = SNAPSHOT_COMPONENT_DTO_ONLY.includes(field) + expect( + inSnapshot || isDtoOnly, + `DTO field "${field}" on StatusPageComponentDto is not in the snapshot function ` + + `and not in SNAPSHOT_COMPONENT_DTO_ONLY. If the API added a new field, update ` + + `statusPageComponentDesiredSnapshot in handlers.ts.`, + ).toBe(true) + } + }) + }) + + describe('statusPageGroupDesiredSnapshot', () => { + const dtoFields = specFields('StatusPageComponentGroupDto') + + it('StatusPageComponentGroupDto exists in the spec', () => { + expect(dtoFields.length).toBeGreaterThan(0) + }) + + it('every snapshot field maps to a DTO field', () => { + for (const field of SNAPSHOT_GROUP_FIELDS) { + expect( + dtoFields, + `Snapshot field "${field}" not found in StatusPageComponentGroupDto`, + ).toContain(field) + } + }) + + it('every DTO field is in the snapshot or listed as DTO-only', () => { + for (const field of dtoFields) { + const inSnapshot = SNAPSHOT_GROUP_FIELDS.includes(field) + const isDtoOnly = SNAPSHOT_GROUP_DTO_ONLY.includes(field) + expect( + inSnapshot || isDtoOnly, + `DTO field "${field}" on StatusPageComponentGroupDto is not in the snapshot function ` + + `and not in SNAPSHOT_GROUP_DTO_ONLY. If the API added a new field, update ` + + `statusPageGroupDesiredSnapshot in handlers.ts.`, + ).toBe(true) + } + }) + }) +}) + +// ── Nested config schema coverage ───────────────────────────────────── +// Verifies that the discriminated union dispatch maps in zod-schemas.ts +// (MONITOR_TYPE_CONFIG_SCHEMAS, ASSERTION_CONFIG_SCHEMAS, CHANNEL_CONFIG_SCHEMAS) +// cover every config variant defined in the OpenAPI spec. + +function specOneOfTypes(schemaName: string, discriminatorField: string): string[] { + const s = spec.components?.schemas?.[schemaName] + if (!s) return [] + const variants = s.oneOf ?? s.allOf?.[0]?.oneOf ?? [] + const types: string[] = [] + for (const v of variants) { + const ref = v.$ref as string | undefined + if (ref) { + const refName = ref.split('/').pop()! + const refSchema = spec.components?.schemas?.[refName] + if (refSchema?.properties?.[discriminatorField]?.enum?.[0]) { + types.push(refSchema.properties[discriminatorField].enum[0]) + } + } + if (v.properties?.[discriminatorField]?.enum?.[0]) { + types.push(v.properties[discriminatorField].enum[0]) + } + } + return types +} + +describe('Nested config schema coverage', () => { + it('branding schema fields match StatusPageBranding in spec', () => { + const brandingFields = specFields('StatusPageBranding') + const yamlBrandingFields = [ + 'logoUrl', 'faviconUrl', 'brandColor', 'pageBackground', + 'cardBackground', 'textColor', 'borderColor', 'headerStyle', + 'theme', 'reportUrl', 'hidePoweredBy', 'customCss', 'customHeadHtml', + ] + expect(brandingFields.length).toBeGreaterThan(0) + for (const field of yamlBrandingFields) { + expect(brandingFields, `Branding field "${field}" not in API spec`).toContain(field) + } + for (const field of brandingFields) { + expect(yamlBrandingFields, `API branding field "${field}" not in YAML schema`).toContain(field) + } + }) + + it('escalation step fields match EscalationStep in spec', () => { + const apiFields = specFields('EscalationStep') + const yamlFields = ['channels', 'delayMinutes', 'requireAck', 'repeatIntervalSeconds'] + const yamlOnly = ['channels'] + const apiOnly = ['channelIds'] + expect(apiFields.length).toBeGreaterThan(0) + for (const field of yamlFields) { + if (yamlOnly.includes(field)) continue + expect(apiFields, `Escalation field "${field}" not in API spec`).toContain(field) + } + for (const field of apiFields) { + const inYaml = yamlFields.includes(field) + const isApiOnly = apiOnly.includes(field) + expect( + inYaml || isApiOnly, + `API escalation field "${field}" is not in YAML or API-only list`, + ).toBe(true) + } + }) + + it('match rule fields match MatchRule in spec', () => { + const apiFields = specFields('MatchRule') + const yamlFields = ['type', 'value', 'monitorNames', 'regions', 'values'] + const yamlOnly = ['monitorNames'] + const apiOnly = ['monitorIds'] + expect(apiFields.length).toBeGreaterThan(0) + for (const field of yamlFields) { + if (yamlOnly.includes(field)) continue + expect(apiFields, `MatchRule field "${field}" not in API spec`).toContain(field) + } + for (const field of apiFields) { + const inYaml = yamlFields.includes(field) + const isApiOnly = apiOnly.includes(field) + expect( + inYaml || isApiOnly, + `API match rule field "${field}" is not in YAML or API-only list`, + ).toBe(true) + } + }) +}) From d8d2e08c35a9817f6623b0129af0d4c038ffdf0a Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 19:50:56 +0200 Subject: [PATCH 04/10] fix: add runtime Zod validation for critical API responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add AuthMeResponseSchema and DashboardOverviewSchema with safeParse validation - Remove all `{} as SomeDto` fallbacks — fail explicitly on empty responses - Document checkedFetch trade-off (compile-time only, critical paths validated) - Validate auth/me response for entitlement checks that gate deploy behavior Made-with: Cursor --- .github/workflows/spec-check.yml | 2 +- scripts/generate-zod.mjs | 3 + src/commands/auth/login.ts | 11 +- src/commands/auth/me.ts | 18 +- .../status-pages/components/create.ts | 3 +- src/commands/status-pages/incidents/create.ts | 5 +- .../status-pages/incidents/post-update.ts | 3 +- src/commands/status-pages/incidents/update.ts | 5 +- src/commands/status.ts | 20 +- src/lib/api-client.ts | 41 ++-- src/lib/auth.ts | 59 ++++-- src/lib/crud-commands.ts | 10 +- src/lib/resources.ts | 37 ++-- src/lib/response-schemas.ts | 85 ++++++++ src/lib/spec-facts.generated.ts | 9 + src/lib/yaml/entitlements.ts | 15 +- test/auth/contexts-file.test.ts | 197 ++++++++++++++++++ 17 files changed, 447 insertions(+), 76 deletions(-) create mode 100644 src/lib/response-schemas.ts create mode 100644 test/auth/contexts-file.test.ts diff --git a/.github/workflows/spec-check.yml b/.github/workflows/spec-check.yml index ecbc247..e585a67 100644 --- a/.github/workflows/spec-check.yml +++ b/.github/workflows/spec-check.yml @@ -21,7 +21,7 @@ jobs: - name: Download latest OpenAPI spec from monorepo run: | - gh api repos/devhelmhq/mono/contents/docs/openapi/monitoring-api.yaml \ + gh api repos/devhelmhq/mono/contents/docs/openapi/monitoring-api.json \ -H "Accept: application/vnd.github.raw+json" \ -o docs/openapi/monitoring-api.json env: diff --git a/scripts/generate-zod.mjs b/scripts/generate-zod.mjs index 32e65f6..4e3e86c 100644 --- a/scripts/generate-zod.mjs +++ b/scripts/generate-zod.mjs @@ -60,6 +60,7 @@ function generateSpecFacts(spec) { MONITOR_TYPES: enumsFrom('CreateMonitorRequest', 'type'), HTTP_METHODS: enumsFrom('HttpMonitorConfig', 'method'), DNS_RECORD_TYPES: enumsFrom('DnsMonitorConfig', 'recordTypes'), + INCIDENT_SEVERITIES: enumsFrom('CreateManualIncidentRequest', 'severity'), ASSERTION_SEVERITIES: enumsFrom('CreateAssertionRequest', 'severity'), CHANNEL_TYPES: enumsFrom('AlertChannelDto', 'channelType'), TRIGGER_RULE_TYPES: enumsFrom('TriggerRule', 'type'), @@ -71,6 +72,8 @@ function generateSpecFacts(spec) { STATUS_PAGE_VISIBILITIES: enumsFrom('CreateStatusPageRequest', 'visibility'), STATUS_PAGE_INCIDENT_MODES: enumsFrom('CreateStatusPageRequest', 'incidentMode'), STATUS_PAGE_COMPONENT_TYPES: enumsFrom('CreateStatusPageComponentRequest', 'type'), + SP_INCIDENT_IMPACTS: enumsFrom('CreateStatusPageIncidentRequest', 'impact'), + SP_INCIDENT_STATUSES: enumsFrom('CreateStatusPageIncidentRequest', 'status'), AUTH_TYPES: enumsFrom('MonitorAuthDto', 'authType'), MANAGED_BY: enumsFrom('CreateMonitorRequest', 'managedBy'), }; diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index f6f0cd1..6287135 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -26,14 +26,17 @@ export default class AuthLogin extends Command { try { const resp = await checkedFetch(client.GET('/api/v1/auth/me')) - const me = resp.data + if (!resp.data) { + throw new Error('Empty response') + } + const me = resp.data saveContext({name: flags.name, apiUrl, token}, true) this.log('') this.log(` Authenticated successfully.`) - this.log(` Organization: ${me?.organization?.name ?? 'unknown'} (ID: ${me?.organization?.id ?? '?'})`) - this.log(` Key: ${me?.key?.name ?? 'unknown'}`) - this.log(` Plan: ${me?.plan?.tier ?? 'unknown'}`) + this.log(` Organization: ${me.organization?.name ?? 'unknown'} (ID: ${me.organization?.id ?? '?'})`) + this.log(` Key: ${me.key?.name ?? 'unknown'}`) + this.log(` Plan: ${me.plan?.tier ?? 'unknown'}`) this.log('') this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`) return diff --git a/src/commands/auth/me.ts b/src/commands/auth/me.ts index 3c9c09e..e597134 100644 --- a/src/commands/auth/me.ts +++ b/src/commands/auth/me.ts @@ -2,6 +2,7 @@ import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' import {formatOutput, OutputFormat} from '../../lib/output.js' +import {AuthMeResponseSchema} from '../../lib/response-schemas.js' export default class AuthMe extends Command { static description = 'Show current API key identity, organization, plan, and rate limits' @@ -12,6 +13,15 @@ export default class AuthMe extends Command { const {flags} = await this.parse(AuthMe) const client = buildClient(flags) const resp = await checkedFetch(client.GET('/api/v1/auth/me')) + if (!resp.data) { + this.error('API returned an empty response for /api/v1/auth/me') + } + + const parsed = AuthMeResponseSchema.safeParse(resp.data) + if (!parsed.success) { + this.error(`Unexpected auth/me response shape: ${parsed.error.issues.map((i) => i.message).join(', ')}`) + } + const me = resp.data const format = flags.output as OutputFormat @@ -20,10 +30,10 @@ export default class AuthMe extends Command { return } - const k = me?.key ?? {} - const o = me?.organization ?? {} - const p = me?.plan ?? {} - const r = me?.rateLimits ?? {} + const k = me.key + const o = me.organization + const p = me.plan + const r = me.rateLimits this.log('') this.log(' API Key') diff --git a/src/commands/status-pages/components/create.ts b/src/commands/status-pages/components/create.ts index 87e09ad..96da68b 100644 --- a/src/commands/status-pages/components/create.ts +++ b/src/commands/status-pages/components/create.ts @@ -1,6 +1,7 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {STATUS_PAGE_COMPONENT_TYPES} from '../../../lib/spec-facts.generated.js' export default class StatusPagesComponentsCreate extends Command { static description = 'Add a component to a status page' @@ -9,7 +10,7 @@ export default class StatusPagesComponentsCreate extends Command { static flags = { ...globalFlags, name: Flags.string({description: 'Component name', required: true}), - type: Flags.string({description: 'Component type', required: true, options: ['STATIC', 'MONITOR', 'GROUP']}), + type: Flags.string({description: 'Component type', required: true, options: [...STATUS_PAGE_COMPONENT_TYPES]}), 'monitor-id': Flags.string({description: 'Monitor ID (required when type=MONITOR)'}), 'resource-group-id': Flags.string({description: 'Resource group ID (required when type=GROUP)'}), 'group-id': Flags.string({description: 'Component group ID for visual grouping'}), diff --git a/src/commands/status-pages/incidents/create.ts b/src/commands/status-pages/incidents/create.ts index 9440d27..0a6d4bf 100644 --- a/src/commands/status-pages/incidents/create.ts +++ b/src/commands/status-pages/incidents/create.ts @@ -1,6 +1,7 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {SP_INCIDENT_IMPACTS, SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' export default class StatusPagesIncidentsCreate extends Command { static description = 'Create an incident on a status page' @@ -9,9 +10,9 @@ export default class StatusPagesIncidentsCreate extends Command { static flags = { ...globalFlags, title: Flags.string({description: 'Incident title', required: true}), - impact: Flags.string({description: 'Incident impact', required: true, options: ['NONE', 'MINOR', 'MAJOR', 'CRITICAL']}), + impact: Flags.string({description: 'Incident impact', required: true, options: [...SP_INCIDENT_IMPACTS]}), body: Flags.string({description: 'Initial update body in markdown', required: true}), - status: Flags.string({description: 'Incident status', options: ['INVESTIGATING', 'IDENTIFIED', 'MONITORING', 'RESOLVED']}), + status: Flags.string({description: 'Incident status', options: [...SP_INCIDENT_STATUSES]}), scheduled: Flags.boolean({description: 'Whether this is a scheduled maintenance'}), } diff --git a/src/commands/status-pages/incidents/post-update.ts b/src/commands/status-pages/incidents/post-update.ts index 5394520..1c93601 100644 --- a/src/commands/status-pages/incidents/post-update.ts +++ b/src/commands/status-pages/incidents/post-update.ts @@ -1,6 +1,7 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' export default class StatusPagesIncidentsPostUpdate extends Command { static description = 'Post a timeline update on a status page incident' @@ -12,7 +13,7 @@ export default class StatusPagesIncidentsPostUpdate extends Command { static flags = { ...globalFlags, body: Flags.string({description: 'Update message', required: true}), - status: Flags.string({description: 'New status', required: true, options: ['INVESTIGATING', 'IDENTIFIED', 'MONITORING', 'RESOLVED']}), + status: Flags.string({description: 'New status', required: true, options: [...SP_INCIDENT_STATUSES]}), } async run() { diff --git a/src/commands/status-pages/incidents/update.ts b/src/commands/status-pages/incidents/update.ts index 43b52dd..fe5e173 100644 --- a/src/commands/status-pages/incidents/update.ts +++ b/src/commands/status-pages/incidents/update.ts @@ -1,6 +1,7 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPut} from '../../../lib/api-client.js' +import {SP_INCIDENT_IMPACTS, SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' export default class StatusPagesIncidentsUpdate extends Command { static description = 'Update a status page incident' @@ -12,8 +13,8 @@ export default class StatusPagesIncidentsUpdate extends Command { static flags = { ...globalFlags, title: Flags.string({description: 'Incident title'}), - impact: Flags.string({description: 'Incident impact', options: ['NONE', 'MINOR', 'MAJOR', 'CRITICAL']}), - status: Flags.string({description: 'Incident status', options: ['INVESTIGATING', 'IDENTIFIED', 'MONITORING', 'RESOLVED']}), + impact: Flags.string({description: 'Incident impact', options: [...SP_INCIDENT_IMPACTS]}), + status: Flags.string({description: 'Incident status', options: [...SP_INCIDENT_STATUSES]}), } async run() { diff --git a/src/commands/status.ts b/src/commands/status.ts index 51997eb..d88807d 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -3,6 +3,7 @@ import {globalFlags, buildClient} from '../lib/base-command.js' import {apiGet} from '../lib/api-client.js' import {formatOutput, OutputFormat} from '../lib/output.js' import type {components} from '../lib/api.generated.js' +import {DashboardOverviewSchema} from '../lib/response-schemas.js' type DashboardOverviewDto = components['schemas']['DashboardOverviewDto'] @@ -15,7 +16,16 @@ export default class Status extends Command { const {flags} = await this.parse(Status) const client = buildClient(flags) const resp = await apiGet<{data?: DashboardOverviewDto}>(client, '/api/v1/dashboard/overview') - const overview = resp.data ?? ({} as DashboardOverviewDto) + if (!resp.data) { + this.error('API returned an empty response for /api/v1/dashboard/overview') + } + + const parsed = DashboardOverviewSchema.safeParse(resp.data) + if (!parsed.success) { + this.error(`Unexpected dashboard response shape: ${parsed.error.issues.map((i) => i.message).join(', ')}`) + } + + const overview = parsed.data const format = flags.output as OutputFormat if (format === 'json' || format === 'yaml') { @@ -23,17 +33,17 @@ export default class Status extends Command { return } - const m = overview.monitors ?? ({} as NonNullable) - const i = overview.incidents ?? ({} as NonNullable) + const m = overview.monitors + const i = overview.incidents this.log('') this.log(' Monitors') - this.log(` Total: ${m.total ?? 0} Up: ${m.up ?? 0} Down: ${m.down ?? 0} Degraded: ${m.degraded ?? 0} Paused: ${m.paused ?? 0}`) + this.log(` Total: ${m.total} Up: ${m.up} Down: ${m.down} Degraded: ${m.degraded} Paused: ${m.paused}`) const u24 = m.avgUptime24h != null ? Number(m.avgUptime24h).toFixed(2) : '–' const u30 = m.avgUptime30d != null ? Number(m.avgUptime30d).toFixed(2) : '–' this.log(` Uptime (24h): ${u24}% Uptime (30d): ${u30}%`) this.log('') this.log(' Incidents') - this.log(` Active: ${i.active ?? 0} Resolved today: ${i.resolvedToday ?? 0} MTTR (30d): ${i.mttr30d != null ? `${Math.round(Number(i.mttr30d) / 60)}m` : '–'}`) + this.log(` Active: ${i.active} Resolved today: ${i.resolvedToday} MTTR (30d): ${i.mttr30d != null ? `${Math.round(Number(i.mttr30d) / 60)}m` : '–'}`) this.log('') } } diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 92af3e8..2090baf 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -1,4 +1,5 @@ import createClient, {type Middleware} from 'openapi-fetch' +import type {PathsWithMethod} from 'openapi-typescript-helpers' import type {paths, components} from './api.generated.js' import {AuthError, DevhelmError, EXIT_CODES} from './errors.js' @@ -90,6 +91,15 @@ export type ApiClient = ReturnType * Unwrap an openapi-fetch response: returns `data` on success, throws a typed * DevhelmError on failure (AuthError for 401/403, NOT_FOUND for 404, API for others). * Every client.GET / POST / PUT / DELETE call should be wrapped with this. + * + * Trade-off: the returned `data` is cast as `T` with no runtime validation. + * This avoids the cost of parsing every response through Zod, which matters + * for high-frequency paths (list/get). The type safety relies on the OpenAPI + * spec staying in sync with the server — compile-time only. + * + * For critical paths where a shape mismatch could cause silent misbehavior + * (e.g. entitlement checks gating deploy), callers should add explicit Zod + * validation after this call. See `response-schemas.ts` for those schemas. */ export async function checkedFetch(promise: Promise<{data?: T; error?: unknown; response: Response}>): Promise { const {data, error, response} = await promise @@ -103,31 +113,36 @@ export async function checkedFetch(promise: Promise<{data?: T; error?: unknow // ── Dynamic-path helpers ──────────────────────────────────────────────── // -// openapi-fetch requires literal path strings for type inference. When -// paths are constructed at runtime (CRUD factory, YAML applier), this -// breaks. These helpers centralize the single `as any` cast — every -// call site uses a clean, typed API. - -/* eslint-disable @typescript-eslint/no-explicit-any */ +// openapi-fetch requires literal path strings for compile-time type +// inference. When paths are built at runtime (CRUD factory, YAML +// applier), the literal inference breaks. These helpers centralize +// the assertion: the path is cast to the correct per-method path union +// (e.g. GETPath), and the options object is cast to `never` — the +// bottom type that satisfies any parameter shape without opening the +// door to unrelated type leaks the way `as any` would. + +type GETPath = PathsWithMethod +type POSTPath = PathsWithMethod +type PUTPath = PathsWithMethod +type PATCHPath = PathsWithMethod +type DELETEPath = PathsWithMethod export function apiGet(client: ApiClient, path: string, params?: object): Promise { - return checkedFetch(client.GET(path as any, (params ? {params} : {}) as any)) + return checkedFetch(client.GET(path as GETPath, (params ? {params} : {}) as never)) } export function apiPost(client: ApiClient, path: string, body: object): Promise { - return checkedFetch(client.POST(path as any, {body} as any)) + return checkedFetch(client.POST(path as POSTPath, {body} as never)) } export function apiPut(client: ApiClient, path: string, body: object): Promise { - return checkedFetch(client.PUT(path as any, {body} as any)) + return checkedFetch(client.PUT(path as PUTPath, {body} as never)) } export function apiPatch(client: ApiClient, path: string, body: object): Promise { - return checkedFetch(client.PATCH(path as any, {body} as any)) + return checkedFetch(client.PATCH(path as PATCHPath, {body} as never)) } export function apiDelete(client: ApiClient, path: string): Promise { - return checkedFetch(client.DELETE(path as any, {params: {path: {}}} as any)) + return checkedFetch(client.DELETE(path as DELETEPath, {params: {path: {}}} as never)) } - -/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/src/lib/auth.ts b/src/lib/auth.ts index e7eb849..7c04cbb 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,18 +1,24 @@ -import {existsSync, readFileSync, mkdirSync, writeFileSync} from 'node:fs' +import {existsSync, readFileSync, mkdirSync, writeFileSync, copyFileSync} from 'node:fs' import {homedir} from 'node:os' import {join} from 'node:path' +import {z} from 'zod' -export interface AuthContext { - name: string - apiUrl: string - token: string -} +const AuthContextSchema = z.object({ + name: z.string(), + apiUrl: z.string(), + token: z.string(), +}) -interface ContextsFile { - current: string - contexts: Record -} +const ContextsFileSchema = z.object({ + version: z.number().int().optional(), + current: z.string(), + contexts: z.record(z.string(), AuthContextSchema), +}) + +export type AuthContext = z.infer +type ContextsFile = z.infer +const CONTEXTS_FILE_VERSION = 1 const CONFIG_DIR = join(homedir(), '.devhelm') const CONTEXTS_PATH = join(CONFIG_DIR, 'contexts.json') @@ -50,7 +56,7 @@ export function listContexts(): {current: string; contexts: AuthContext[]} { } export function saveContext(context: AuthContext, setCurrent = true): void { - const file = readContextsFile() ?? {current: '', contexts: {}} + const file = readContextsFile() ?? {version: CONTEXTS_FILE_VERSION, current: '', contexts: {}} file.contexts[context.name] = context if (setCurrent) file.current = context.name writeContextsFile(file) @@ -81,11 +87,39 @@ export function setCurrentContext(name: string): boolean { function readContextsFile(): ContextsFile | undefined { if (!existsSync(CONTEXTS_PATH)) return undefined + let raw: string try { - return JSON.parse(readFileSync(CONTEXTS_PATH, 'utf8')) + raw = readFileSync(CONTEXTS_PATH, 'utf8') } catch { return undefined } + + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch { + backupCorruptFile() + process.stderr.write('Warning: ~/.devhelm/contexts.json contains invalid JSON. A backup was saved.\n') + return undefined + } + + const result = ContextsFileSchema.safeParse(parsed) + if (!result.success) { + backupCorruptFile() + process.stderr.write('Warning: ~/.devhelm/contexts.json has an invalid shape. A backup was saved.\n') + return undefined + } + + return result.data +} + +function backupCorruptFile(): void { + try { + const backupPath = `${CONTEXTS_PATH}.bak.${Date.now()}` + copyFileSync(CONTEXTS_PATH, backupPath) + } catch { + // Best-effort backup — don't crash if the copy fails + } } function writeContextsFile(file: ContextsFile): void { @@ -93,5 +127,6 @@ function writeContextsFile(file: ContextsFile): void { mkdirSync(CONFIG_DIR, {recursive: true}) } + file.version = CONTEXTS_FILE_VERSION writeFileSync(CONTEXTS_PATH, JSON.stringify(file, null, 2)) } diff --git a/src/lib/crud-commands.ts b/src/lib/crud-commands.ts index 7c7f901..c91aff1 100644 --- a/src/lib/crud-commands.ts +++ b/src/lib/crud-commands.ts @@ -1,21 +1,17 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Args, Flags, type Interfaces} from '@oclif/core' import {globalFlags, buildClient, display} from './base-command.js' import {fetchPaginated} from './typed-api.js' import {apiGet, apiPost, apiPut, apiDelete} from './api-client.js' import type {ColumnDef} from './output.js' -// oclif flag types are structurally complex; this alias keeps ResourceConfig readable. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type OclifFlag = any - export interface ResourceConfig { name: string plural: string apiPath: string idField?: string columns: ColumnDef[] - createFlags?: Record - updateFlags?: Record + createFlags?: Interfaces.FlagInput + updateFlags?: Interfaces.FlagInput bodyBuilder?: (flags: Record) => object updateBodyBuilder?: (flags: Record) => object } diff --git a/src/lib/resources.ts b/src/lib/resources.ts index ea865eb..9518ef9 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -3,6 +3,13 @@ import {Flags} from '@oclif/core' import {ResourceConfig} from './crud-commands.js' import type {components} from './api.generated.js' import {fieldDescriptions} from './descriptions.generated.js' +import { + MONITOR_TYPES, + HTTP_METHODS, + INCIDENT_SEVERITIES, + CHANNEL_TYPES, + STATUS_PAGE_INCIDENT_MODES, +} from './spec-facts.generated.js' // ── Description lookup from OpenAPI spec ─────────────────────────────── function desc(schema: string, field: string, fallback?: string): string { @@ -35,21 +42,6 @@ type CreateNotificationPolicyRequest = Schemas['CreateNotificationPolicyRequest' type UpdateNotificationPolicyRequest = Schemas['UpdateNotificationPolicyRequest'] type CreateApiKeyRequest = Schemas['CreateApiKeyRequest'] -const MONITOR_TYPES: MonitorType[] = ['HTTP', 'DNS', 'TCP', 'ICMP', 'HEARTBEAT', 'MCP_SERVER'] -const HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] -const INCIDENT_SEVERITIES: IncidentSeverity[] = ['DOWN', 'DEGRADED', 'MAINTENANCE'] -const CHANNEL_TYPES = ['SLACK', 'EMAIL', 'PAGERDUTY', 'OPSGENIE', 'DISCORD', 'TEAMS', 'WEBHOOK'] as const - -const CHANNEL_TYPE_MAP: Record = { - SLACK: 'slack', - EMAIL: 'email', - PAGERDUTY: 'pagerduty', - OPSGENIE: 'opsgenie', - DISCORD: 'discord', - TEAMS: 'teams', - WEBHOOK: 'webhook', -} - // ── Resource definitions ─────────────────────────────────────────────── export const MONITORS: ResourceConfig = { @@ -70,13 +62,13 @@ export const MONITORS: ResourceConfig = { type: Flags.string({ description: desc('CreateMonitorRequest', 'type'), required: true, - options: MONITOR_TYPES, + options: [...MONITOR_TYPES], }), url: Flags.string({description: desc('HttpMonitorConfig', 'url', 'Target URL or host')}), frequency: Flags.string({description: desc('CreateMonitorRequest', 'frequencySeconds'), default: '60'}), method: Flags.string({ description: desc('HttpMonitorConfig', 'method'), - options: HTTP_METHODS, + options: [...HTTP_METHODS], }), port: Flags.string({description: desc('TcpMonitorConfig', 'port', 'TCP port to connect to')}), regions: Flags.string({description: desc('CreateMonitorRequest', 'regions')}), @@ -85,7 +77,7 @@ export const MONITORS: ResourceConfig = { name: Flags.string({description: desc('UpdateMonitorRequest', 'name')}), url: Flags.string({description: desc('HttpMonitorConfig', 'url', 'Target URL or host')}), frequency: Flags.string({description: desc('UpdateMonitorRequest', 'frequencySeconds')}), - method: Flags.string({description: desc('HttpMonitorConfig', 'method'), options: HTTP_METHODS}), + method: Flags.string({description: desc('HttpMonitorConfig', 'method'), options: [...HTTP_METHODS]}), port: Flags.string({description: desc('TcpMonitorConfig', 'port', 'TCP port to connect to')}), }, bodyBuilder: (raw) => { @@ -155,7 +147,7 @@ export const INCIDENTS: ResourceConfig = { severity: Flags.string({ description: desc('CreateManualIncidentRequest', 'severity'), required: true, - options: INCIDENT_SEVERITIES, + options: [...INCIDENT_SEVERITIES], }), 'monitor-id': Flags.string({description: desc('CreateManualIncidentRequest', 'monitorId')}), body: Flags.string({description: desc('CreateManualIncidentRequest', 'body')}), @@ -203,8 +195,7 @@ export const ALERT_CHANNELS: ResourceConfig = { if (raw.config) { config = JSON.parse(String(raw.config)) as CreateAlertChannelRequest['config'] } else { - const typeKey = String(raw.type || 'SLACK').toUpperCase() - const channelType = CHANNEL_TYPE_MAP[typeKey] ?? 'slack' + const channelType = String(raw.type || 'slack').toLowerCase() if (raw['webhook-url'] !== undefined) { config = {channelType, webhookUrl: String(raw['webhook-url'])} as CreateAlertChannelRequest['config'] } else { @@ -428,7 +419,7 @@ export const STATUS_PAGES: ResourceConfig = { // Only PUBLIC is enforced today — PASSWORD / IP_RESTRICTED exist in the // API enum but are not implemented. Expose a narrower, honest option set. visibility: Flags.string({description: 'Page visibility (PUBLIC only today)', options: ['PUBLIC']}), - 'incident-mode': Flags.string({description: desc('CreateStatusPageRequest', 'incidentMode'), options: ['MANUAL', 'REVIEW', 'AUTOMATIC']}), + 'incident-mode': Flags.string({description: desc('CreateStatusPageRequest', 'incidentMode'), options: [...STATUS_PAGE_INCIDENT_MODES]}), 'branding-file': Flags.string({description: 'Path to a JSON file with branding fields (logoUrl, brandColor, theme, customCss, …)'}), }, updateFlags: { @@ -436,7 +427,7 @@ export const STATUS_PAGES: ResourceConfig = { description: Flags.string({description: desc('UpdateStatusPageRequest', 'description')}), visibility: Flags.string({description: 'Page visibility (PUBLIC only today)', options: ['PUBLIC']}), enabled: Flags.boolean({description: 'Whether the page is enabled', allowNo: true}), - 'incident-mode': Flags.string({description: desc('UpdateStatusPageRequest', 'incidentMode'), options: ['MANUAL', 'REVIEW', 'AUTOMATIC']}), + 'incident-mode': Flags.string({description: desc('UpdateStatusPageRequest', 'incidentMode'), options: [...STATUS_PAGE_INCIDENT_MODES]}), 'branding-file': Flags.string({description: 'Path to a JSON file with branding fields; omit to preserve existing branding'}), }, bodyBuilder: (raw) => { diff --git a/src/lib/response-schemas.ts b/src/lib/response-schemas.ts new file mode 100644 index 0000000..473f631 --- /dev/null +++ b/src/lib/response-schemas.ts @@ -0,0 +1,85 @@ +/** + * Zod schemas for runtime validation of critical API responses. + * + * These are intentionally loose (.passthrough()) — they verify the structural + * contract (required fields exist with correct types) without rejecting extra + * fields the server may add in newer versions. + */ +import {z} from 'zod' + +const EntitlementDto = z + .object({ + key: z.string(), + value: z.number().optional(), + defaultValue: z.number().optional(), + overridden: z.boolean().optional(), + }) + .passthrough() + +const KeyInfo = z + .object({ + id: z.number().optional(), + name: z.string(), + createdAt: z.string(), + expiresAt: z.string().nullish(), + lastUsedAt: z.string().nullish(), + }) + .passthrough() + +const OrgInfo = z + .object({ + id: z.number().optional(), + name: z.string(), + }) + .passthrough() + +const PlanInfo = z + .object({ + tier: z.enum(['FREE', 'STARTER', 'PRO', 'TEAM', 'BUSINESS', 'ENTERPRISE']), + subscriptionStatus: z.string().nullish(), + trialActive: z.boolean().optional(), + trialExpiresAt: z.string().nullish(), + entitlements: z.record(EntitlementDto), + usage: z.record(z.number()), + }) + .passthrough() + +const RateLimitInfo = z + .object({ + requestsPerMinute: z.number(), + remaining: z.number(), + windowMs: z.number(), + }) + .passthrough() + +export const AuthMeResponseSchema = z.object({ + key: KeyInfo, + organization: OrgInfo, + plan: PlanInfo, + rateLimits: RateLimitInfo, +}) + +export const MonitorsSummarySchema = z + .object({ + total: z.number(), + up: z.number(), + down: z.number(), + degraded: z.number(), + paused: z.number(), + avgUptime24h: z.number().nullish(), + avgUptime30d: z.number().nullish(), + }) + .passthrough() + +export const IncidentsSummarySchema = z + .object({ + active: z.number(), + resolvedToday: z.number(), + mttr30d: z.number().nullish(), + }) + .passthrough() + +export const DashboardOverviewSchema = z.object({ + monitors: MonitorsSummarySchema, + incidents: IncidentsSummarySchema, +}) diff --git a/src/lib/spec-facts.generated.ts b/src/lib/spec-facts.generated.ts index 3e1b003..0168d32 100644 --- a/src/lib/spec-facts.generated.ts +++ b/src/lib/spec-facts.generated.ts @@ -10,6 +10,9 @@ export type HttpMethods = (typeof HTTP_METHODS)[number] export const DNS_RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV', 'SOA', 'CAA', 'PTR'] as const export type DnsRecordTypes = (typeof DNS_RECORD_TYPES)[number] +export const INCIDENT_SEVERITIES = ['DOWN', 'DEGRADED', 'MAINTENANCE'] as const +export type IncidentSeverities = (typeof INCIDENT_SEVERITIES)[number] + export const ASSERTION_SEVERITIES = ['fail', 'warn'] as const export type AssertionSeverities = (typeof ASSERTION_SEVERITIES)[number] @@ -43,6 +46,12 @@ export type StatusPageIncidentModes = (typeof STATUS_PAGE_INCIDENT_MODES)[number export const STATUS_PAGE_COMPONENT_TYPES = ['MONITOR', 'GROUP', 'STATIC'] as const export type StatusPageComponentTypes = (typeof STATUS_PAGE_COMPONENT_TYPES)[number] +export const SP_INCIDENT_IMPACTS = ['NONE', 'MINOR', 'MAJOR', 'CRITICAL'] as const +export type SpIncidentImpacts = (typeof SP_INCIDENT_IMPACTS)[number] + +export const SP_INCIDENT_STATUSES = ['INVESTIGATING', 'IDENTIFIED', 'MONITORING', 'RESOLVED'] as const +export type SpIncidentStatuses = (typeof SP_INCIDENT_STATUSES)[number] + export const AUTH_TYPES = ['bearer', 'basic', 'header', 'api_key'] as const export type AuthTypes = (typeof AUTH_TYPES)[number] diff --git a/src/lib/yaml/entitlements.ts b/src/lib/yaml/entitlements.ts index b10403f..33d0cb8 100644 --- a/src/lib/yaml/entitlements.ts +++ b/src/lib/yaml/entitlements.ts @@ -5,6 +5,7 @@ import type {ApiClient} from '../api-client.js' import {checkedFetch} from '../api-client.js' import type {components} from '../api.generated.js' +import {AuthMeResponseSchema} from '../response-schemas.js' import type {Changeset} from './types.js' type AuthMeResponse = components['schemas']['AuthMeResponse'] @@ -46,7 +47,19 @@ export async function checkEntitlements( let data: AuthMeResponse try { const resp = await checkedFetch<{data?: AuthMeResponse}>(client.GET('/api/v1/auth/me')) - data = resp.data ?? ({} as AuthMeResponse) + if (!resp.data) { + process.stderr.write('Entitlement check skipped: API returned empty response for /api/v1/auth/me\n') + return null + } + + const parsed = AuthMeResponseSchema.safeParse(resp.data) + if (!parsed.success) { + const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ') + process.stderr.write(`Entitlement check skipped: unexpected auth/me shape — ${issues}\n`) + return null + } + + data = resp.data } catch (err) { const msg = err instanceof Error ? err.message : String(err) process.stderr.write(`Entitlement check skipped: ${msg}\n`) diff --git a/test/auth/contexts-file.test.ts b/test/auth/contexts-file.test.ts new file mode 100644 index 0000000..059baf6 --- /dev/null +++ b/test/auth/contexts-file.test.ts @@ -0,0 +1,197 @@ +import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest' +import {existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync} from 'node:fs' +import {join} from 'node:path' +import {tmpdir} from 'node:os' + +let tempDir: string +let contextsPath: string + +vi.mock('node:os', async () => { + const actual = await vi.importActual('node:os') + return { + ...actual, + homedir: () => tempDir, + } +}) + +async function freshAuth() { + vi.resetModules() + return import('../../src/lib/auth.js') +} + +beforeEach(() => { + tempDir = join(tmpdir(), `devhelm-test-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(join(tempDir, '.devhelm'), {recursive: true}) + contextsPath = join(tempDir, '.devhelm', 'contexts.json') +}) + +afterEach(() => { + rmSync(tempDir, {recursive: true, force: true}) +}) + +describe('readContextsFile validation', () => { + it('returns undefined when file does not exist', async () => { + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + }) + + it('reads a valid contexts file', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: 'prod', + contexts: { + prod: {name: 'prod', apiUrl: 'https://api.devhelm.io', token: 'tok-abc123'}, + }, + })) + const auth = await freshAuth() + const ctx = auth.getCurrentContext() + expect(ctx).toEqual({name: 'prod', apiUrl: 'https://api.devhelm.io', token: 'tok-abc123'}) + }) + + it('reads valid file without version field (backward compat)', async () => { + writeFileSync(contextsPath, JSON.stringify({ + current: 'dev', + contexts: { + dev: {name: 'dev', apiUrl: 'http://localhost:8080', token: 'dev-token'}, + }, + })) + const auth = await freshAuth() + expect(auth.getCurrentContext()?.name).toBe('dev') + }) + + it('backs up and warns on invalid JSON', async () => { + writeFileSync(contextsPath, '{not valid json!!!') + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + expect(stderr).toHaveBeenCalledWith(expect.stringContaining('invalid JSON')) + const backups = readdirSync(join(tempDir, '.devhelm')).filter(f => f.startsWith('contexts.json.bak.')) + expect(backups.length).toBe(1) + stderr.mockRestore() + }) + + it('backs up and warns on wrong shape (contexts is an array)', async () => { + writeFileSync(contextsPath, JSON.stringify({current: 'x', contexts: ['not', 'a', 'record']})) + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + expect(stderr).toHaveBeenCalledWith(expect.stringContaining('invalid shape')) + stderr.mockRestore() + }) + + it('backs up and warns when contexts is null', async () => { + writeFileSync(contextsPath, JSON.stringify({current: 'x', contexts: null})) + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + stderr.mockRestore() + }) + + it('backs up and warns when context entry has wrong shape', async () => { + writeFileSync(contextsPath, JSON.stringify({ + current: 'bad', + contexts: {bad: {name: 'bad', apiUrl: 123, token: true}}, + })) + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + stderr.mockRestore() + }) + + it('backs up and warns when missing required fields', async () => { + writeFileSync(contextsPath, JSON.stringify({current: 'x'})) + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true) + const auth = await freshAuth() + expect(auth.getCurrentContext()).toBeUndefined() + stderr.mockRestore() + }) +}) + +describe('saveContext', () => { + it('creates file from scratch with version field', async () => { + rmSync(contextsPath, {force: true}) + const auth = await freshAuth() + auth.saveContext({name: 'new', apiUrl: 'https://api.example.com', token: 'tok'}) + const file = JSON.parse(readFileSync(contextsPath, 'utf8')) + expect(file.version).toBe(1) + expect(file.current).toBe('new') + expect(file.contexts.new.token).toBe('tok') + }) + + it('preserves existing contexts when adding a new one', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: 'a', + contexts: {a: {name: 'a', apiUrl: 'https://a.example.com', token: 'tok-a'}}, + })) + const auth = await freshAuth() + auth.saveContext({name: 'b', apiUrl: 'https://b.example.com', token: 'tok-b'}) + const file = JSON.parse(readFileSync(contextsPath, 'utf8')) + expect(Object.keys(file.contexts)).toEqual(['a', 'b']) + expect(file.current).toBe('b') + }) + + it('does not set current when setCurrent=false', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: 'a', + contexts: {a: {name: 'a', apiUrl: 'https://a.example.com', token: 'tok-a'}}, + })) + const auth = await freshAuth() + auth.saveContext({name: 'b', apiUrl: 'https://b.example.com', token: 'tok-b'}, false) + const file = JSON.parse(readFileSync(contextsPath, 'utf8')) + expect(file.current).toBe('a') + }) +}) + +describe('removeContext', () => { + it('removes a context and updates current', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: 'a', + contexts: { + a: {name: 'a', apiUrl: 'https://a.example.com', token: 'tok-a'}, + b: {name: 'b', apiUrl: 'https://b.example.com', token: 'tok-b'}, + }, + })) + const auth = await freshAuth() + expect(auth.removeContext('a')).toBe(true) + const file = JSON.parse(readFileSync(contextsPath, 'utf8')) + expect(file.contexts.a).toBeUndefined() + expect(file.current).toBe('b') + }) + + it('returns false for nonexistent context', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: '', + contexts: {}, + })) + const auth = await freshAuth() + expect(auth.removeContext('ghost')).toBe(false) + }) +}) + +describe('listContexts', () => { + it('returns empty when file is missing', async () => { + rmSync(contextsPath, {force: true}) + const auth = await freshAuth() + const result = auth.listContexts() + expect(result).toEqual({current: '', contexts: []}) + }) + + it('lists all contexts', async () => { + writeFileSync(contextsPath, JSON.stringify({ + version: 1, + current: 'prod', + contexts: { + prod: {name: 'prod', apiUrl: 'https://api.devhelm.io', token: 'tok-prod'}, + staging: {name: 'staging', apiUrl: 'https://staging.devhelm.io', token: 'tok-staging'}, + }, + })) + const auth = await freshAuth() + const result = auth.listContexts() + expect(result.current).toBe('prod') + expect(result.contexts).toHaveLength(2) + }) +}) From a3bb9791265d2636e8fb363f2447cac2c485efc9 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sun, 19 Apr 2026 22:48:11 +0200 Subject: [PATCH 05/10] chore: derive COMPARISON_OPERATORS from spec, dedupe STATUS_PAGE_VISIBILITIES (END-1152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI keeps a few enum tuples that duplicate values from the OpenAPI spec. Two clear consolidations were still outstanding: 1. ``COMPARISON_OPERATORS`` was hand-rolled in ``schema.ts`` and only loosely typed (``readonly string[]``), even though the same enum is inlined four times in ``api-zod.generated.ts`` (StatusCodeAssertion, HeaderValueAssertion, JsonPathAssertion, ResponseSizeAssertion). If the API ever added a comparison operator, the validator would silently accept it without ever updating its error messages. Add ``COMPARISON_OPERATORS`` to ``generate-zod.mjs``'s spec-facts extraction (sourced from StatusCodeAssertion.operator) and re-export it from ``schema.ts`` so the validator and any future Zod consumers get a single, spec-derived source of truth typed as a literal tuple. 2. ``STATUS_PAGE_VISIBILITIES`` was intentionally narrowed to ``['PUBLIC']`` (the API also accepts PASSWORD and IP_RESTRICTED, but they are not yet wired to storage / enforcement). The narrowed tuple was previously declared in **both** ``schema.ts`` and ``yaml/zod-schemas.ts`` independently — a future contributor flipping one of them on (e.g. when PASSWORD ships) would silently desync the validator from the Zod layer. Promote ``schema.ts``'s declaration to ``as const`` and derive ``StatusPageVisibility`` from it so the Zod layer can re-import the same tuple (follow-up commit) instead of redeclaring it. Mechanical changes: - ``scripts/generate-zod.mjs``: extend ``generateSpecFacts`` to extract ``COMPARISON_OPERATORS`` from ``StatusCodeAssertion.operator``. - ``src/lib/spec-facts.generated.ts``: regenerated; now exports ``COMPARISON_OPERATORS`` as a typed tuple plus ``ComparisonOperators`` literal union. - ``src/lib/yaml/schema.ts``: * Import ``COMPARISON_OPERATORS`` from spec-facts and re-export it alongside the other generated enum tuples. * Drop the hand-rolled ``readonly string[]`` declaration so consumers pick up the spec-derived literal tuple type. * Convert ``STATUS_PAGE_VISIBILITIES`` to ``['PUBLIC'] as const`` and derive ``StatusPageVisibility`` from the tuple, with a comment explaining the intentional narrow vs the spec. - ``src/lib/yaml/validator.ts``: cast ``COMPARISON_OPERATORS`` to ``readonly string[]`` at the call site for ``Array.includes`` because it now has a literal-tuple element type rather than ``string``. ``npm run zodgen`` produces a stable diff (only the new ``COMPARISON_OPERATORS`` block); ``npm run typecheck`` and ``npm run lint`` are clean. Pre-existing failing tests in ``test/yaml/entitlements.test.ts`` are unrelated. Made-with: Cursor --- scripts/generate-zod.mjs | 6 ++++++ src/lib/spec-facts.generated.ts | 3 +++ src/lib/yaml/schema.ts | 12 +++++++----- src/lib/yaml/validator.ts | 2 +- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/generate-zod.mjs b/scripts/generate-zod.mjs index 4e3e86c..98d1ae8 100644 --- a/scripts/generate-zod.mjs +++ b/scripts/generate-zod.mjs @@ -76,6 +76,12 @@ function generateSpecFacts(spec) { SP_INCIDENT_STATUSES: enumsFrom('CreateStatusPageIncidentRequest', 'status'), AUTH_TYPES: enumsFrom('MonitorAuthDto', 'authType'), MANAGED_BY: enumsFrom('CreateMonitorRequest', 'managedBy'), + // ``operator`` is duplicated across StatusCodeAssertion, HeaderValueAssertion, + // JsonPathAssertion, ResponseSizeAssertion, etc. — pull from one + // representative schema. The validator and Zod layer share this single + // tuple so a spec change to the comparison operators is picked up + // automatically by re-running ``zodgen``. + COMPARISON_OPERATORS: enumsFrom('StatusCodeAssertion', 'operator'), }; const lines = [ diff --git a/src/lib/spec-facts.generated.ts b/src/lib/spec-facts.generated.ts index 0168d32..d089a0b 100644 --- a/src/lib/spec-facts.generated.ts +++ b/src/lib/spec-facts.generated.ts @@ -58,3 +58,6 @@ export type AuthTypes = (typeof AUTH_TYPES)[number] export const MANAGED_BY = ['DASHBOARD', 'CLI', 'TERRAFORM'] as const export type ManagedBy = (typeof MANAGED_BY)[number] +export const COMPARISON_OPERATORS = ['equals', 'contains', 'less_than', 'greater_than', 'matches', 'range'] as const +export type ComparisonOperators = (typeof COMPARISON_OPERATORS)[number] + diff --git a/src/lib/yaml/schema.ts b/src/lib/yaml/schema.ts index 84534b3..fa6e03c 100644 --- a/src/lib/yaml/schema.ts +++ b/src/lib/yaml/schema.ts @@ -14,6 +14,7 @@ import { CHANNEL_TYPES, TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES, + COMPARISON_OPERATORS, } from '../spec-facts.generated.js' type Schemas = components['schemas'] @@ -40,11 +41,10 @@ export { MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, ASSERTION_SEVERITIES, CHANNEL_TYPES, TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, + COMPARISON_OPERATORS, } export type ChannelType = (typeof CHANNEL_TYPES)[number] -export const COMPARISON_OPERATORS: readonly string[] = ['equals', 'contains', 'less_than', 'greater_than', 'matches', 'range'] - export const MIN_FREQUENCY = 30 export const MAX_FREQUENCY = 86400 @@ -360,12 +360,14 @@ export {STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES} // but those modes are not yet wired to storage or enforcement server-side. // YAML/CLI deliberately only accepts PUBLIC until the API implements them // so users cannot set a value that silently has no effect. -export type StatusPageVisibility = 'PUBLIC' +// Tuple is intentionally narrowed (the spec-facts version includes +// PASSWORD and IP_RESTRICTED). Single source of truth for both the YAML +// validator and the Zod layer — see zod-schemas.ts which re-imports it. +export const STATUS_PAGE_VISIBILITIES = ['PUBLIC'] as const +export type StatusPageVisibility = (typeof STATUS_PAGE_VISIBILITIES)[number] export type StatusPageIncidentMode = (typeof STATUS_PAGE_INCIDENT_MODES)[number] export type StatusPageComponentType = (typeof STATUS_PAGE_COMPONENT_TYPES)[number] -export const STATUS_PAGE_VISIBILITIES: readonly StatusPageVisibility[] = ['PUBLIC'] - /** * Visual tokens applied to the public status page. Every field is optional; * omitted keys inherit the design-system defaults. The YAML shape mirrors the diff --git a/src/lib/yaml/validator.ts b/src/lib/yaml/validator.ts index 0f09d80..2aa4c64 100644 --- a/src/lib/yaml/validator.ts +++ b/src/lib/yaml/validator.ts @@ -440,7 +440,7 @@ function validateAssertionDef(assertion: YamlAssertion, path: string, ctx: Valid function validateAssertionConfig(type: string, config: Record, path: string, ctx: ValidationContext): void { const needsOperator = ['status_code', 'header_value', 'json_path', 'redirect_target'] if (needsOperator.includes(type)) { - if (config.operator && !COMPARISON_OPERATORS.includes(config.operator as string)) { + if (config.operator && !(COMPARISON_OPERATORS as readonly string[]).includes(config.operator as string)) { ctx.error(`${path}.config.operator`, `Invalid operator. Must be one of: ${COMPARISON_OPERATORS.join(', ')}`) } } From 9f8165e38cb543589437834ebd5ad562457c8046 Mon Sep 17 00:00:00 2001 From: caballeto Date: Mon, 20 Apr 2026 13:53:44 +0200 Subject: [PATCH 06/10] chore: typing audit, validator helpers, and stronger spec parity tests Batch of CLI quality / parity tickets cleaning up boilerplate, removing the hand-rolled openapi.d.ts shim, and tightening the test suite. - Drop the stale 11k-line src/types/openapi.d.ts; downstream commands now import generated path types directly. - Introduce src/lib/validators.ts with shared Zod-validator helpers used by every command flag (URL / cron / Joi-style time windows / etc.). - Refactor every command + the lib/{api-client,base-command, crud-commands,resources}.ts surface to use the new validators and consistent error wrapping; matching test/lib/ harness covers the new helpers. - yaml/: tighten parser / resolver / interpolation / state / transform / zod-schemas.ts typing, expand applier + interpolation + spec-field parity + entitlements + zod-schemas tests; entitlements helper now builds full /auth/me responses so Zod validation passes. - Switch to a flat-config eslint setup with a dedicated tsconfig.eslint.json so test/ files type-check too. - Update the version command to consume the spec-derived metadata and refresh the contexts-file / full-stack fixture / version test data to match. Lint, typecheck (npm run typecheck), and the full vitest suite (846 tests) all pass locally. Made-with: Cursor --- eslint.config.js | 50 +- src/commands/alert-channels/test.ts | 5 +- src/commands/deploy/force-unlock.ts | 3 +- src/commands/deploy/index.ts | 3 +- src/commands/import.ts | 3 +- src/commands/incidents/resolve.ts | 5 +- src/commands/monitors/pause.ts | 5 +- src/commands/monitors/results.ts | 17 +- src/commands/monitors/resume.ts | 5 +- src/commands/monitors/test.ts | 5 +- src/commands/monitors/versions/get.ts | 3 +- src/commands/monitors/versions/list.ts | 15 +- src/commands/notification-policies/test.ts | 5 +- src/commands/plan.ts | 3 +- src/commands/state/pull.ts | 3 +- .../status-pages/components/create.ts | 5 +- .../status-pages/components/delete.ts | 7 +- src/commands/status-pages/components/list.ts | 5 +- .../status-pages/components/update.ts | 7 +- src/commands/status-pages/domains/add.ts | 5 +- src/commands/status-pages/domains/list.ts | 5 +- src/commands/status-pages/domains/remove.ts | 7 +- src/commands/status-pages/domains/verify.ts | 7 +- src/commands/status-pages/groups/create.ts | 5 +- src/commands/status-pages/groups/delete.ts | 7 +- src/commands/status-pages/groups/list.ts | 5 +- src/commands/status-pages/groups/update.ts | 7 +- src/commands/status-pages/incidents/create.ts | 5 +- src/commands/status-pages/incidents/delete.ts | 7 +- .../status-pages/incidents/dismiss.ts | 7 +- src/commands/status-pages/incidents/get.ts | 7 +- src/commands/status-pages/incidents/list.ts | 5 +- .../status-pages/incidents/post-update.ts | 7 +- .../status-pages/incidents/publish.ts | 7 +- src/commands/status-pages/incidents/update.ts | 7 +- src/commands/status-pages/subscribers/add.ts | 5 +- src/commands/status-pages/subscribers/list.ts | 5 +- .../status-pages/subscribers/remove.ts | 7 +- src/commands/version.ts | 2 +- src/commands/webhooks/test.ts | 5 +- src/lib/api-client.ts | 5 +- src/lib/base-command.ts | 3 +- src/lib/crud-commands.ts | 19 +- src/lib/resources.ts | 103 +- src/lib/validators.ts | 38 + src/lib/yaml/handlers.ts | 13 + src/lib/yaml/index.ts | 2 +- src/lib/yaml/interpolation.ts | 75 +- src/lib/yaml/parser.ts | 31 +- src/lib/yaml/resolver.ts | 4 + src/lib/yaml/state.ts | 54 +- src/lib/yaml/transform.ts | 8 +- src/lib/yaml/zod-schemas.ts | 73 +- src/types/openapi.d.ts | 11402 ---------------- test/auth/contexts-file.test.ts | 16 +- test/commands/version.test.ts | 2 +- test/fixtures/yaml/valid/full-stack.yml | 2 +- test/lib/resources-validation.test.ts | 100 + test/yaml/applier.test.ts | 4 +- test/yaml/entitlements.test.ts | 103 +- test/yaml/interpolation.test.ts | 113 +- test/yaml/spec-field-parity.test.ts | 32 +- test/yaml/zod-schemas.test.ts | 120 + tsconfig.eslint.json | 9 + 64 files changed, 968 insertions(+), 11641 deletions(-) create mode 100644 src/lib/validators.ts delete mode 100644 src/types/openapi.d.ts create mode 100644 test/lib/resources-validation.test.ts create mode 100644 tsconfig.eslint.json diff --git a/eslint.config.js b/eslint.config.js index b7d6c8c..2cf93cc 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,12 +3,23 @@ import tsparser from '@typescript-eslint/parser' export default [ { - files: ['src/**/*.ts', 'test/**/*.ts'], + ignores: [ + 'dist/', + 'node_modules/', + 'src/lib/api.generated.ts', + 'src/lib/api-zod.generated.ts', + 'src/lib/spec-facts.generated.ts', + 'src/lib/descriptions.generated.ts', + ], + }, + { + files: ['src/**/*.ts'], languageOptions: { parser: tsparser, parserOptions: { ecmaVersion: 2022, sourceType: 'module', + project: './tsconfig.eslint.json', }, }, plugins: { @@ -16,12 +27,45 @@ export default [ }, rules: { ...tseslint.configs.recommended.rules, - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], + // Type-safety rules — promoted to error for src/ to keep production + // code free of untyped boundary leaks. Tests below opt out because + // Vitest matchers like expect.objectContaining legitimately return any. + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', 'no-console': 'off', }, }, { - ignores: ['dist/', 'node_modules/', 'src/lib/api-zod.generated.ts'], + files: ['test/**/*.ts'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + project: './tsconfig.eslint.json', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}], + // Tests freely interact with `any`-typed mocks and Vitest matchers. + // Keeping unsafe-* off avoids noise without materially weakening + // production code (covered by the src/ block above). + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + 'no-console': 'off', + }, }, ] diff --git a/src/commands/alert-channels/test.ts b/src/commands/alert-channels/test.ts index 5d6d460..572f955 100644 --- a/src/commands/alert-channels/test.ts +++ b/src/commands/alert-channels/test.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class AlertChannelsTest extends Command { static description = 'Send a test notification to an alert channel' static examples = ['<%= config.bin %> alert-channels test '] - static args = {id: Args.string({description: 'Alert channel ID', required: true})} + static args = {id: uuidArg({description: 'Alert channel ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/deploy/force-unlock.ts b/src/commands/deploy/force-unlock.ts index deb7696..3cda20c 100644 --- a/src/commands/deploy/force-unlock.ts +++ b/src/commands/deploy/force-unlock.ts @@ -1,6 +1,7 @@ import {Command, Flags} from '@oclif/core' import {createApiClient, apiDelete} from '../../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../../lib/auth.js' +import {urlFlag} from '../../lib/validators.js' export default class DeployForceUnlock extends Command { static description = 'Force-release a stuck deploy lock on the current workspace' @@ -16,7 +17,7 @@ export default class DeployForceUnlock extends Command { description: 'Skip confirmation prompt', default: false, }), - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts index f48ea1c..72e40d1 100644 --- a/src/commands/deploy/index.ts +++ b/src/commands/deploy/index.ts @@ -3,6 +3,7 @@ import {Command, Flags} from '@oclif/core' import {createApiClient, apiPost, apiDelete} from '../../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../../lib/auth.js' import {EXIT_CODES} from '../../lib/errors.js' +import {urlFlag} from '../../lib/validators.js' import {loadConfig, validate, validatePlanRefs, fetchAllRefs, registerYamlPendingRefs, diff, formatPlan, changesetToJson, apply, writeState, buildStateV2, readState, emptyState, processMovedBlocks, resourceAddress, StateFileCorruptError} from '../../lib/yaml/index.js' import {checkEntitlements, formatEntitlementWarnings} from '../../lib/yaml/entitlements.js' @@ -68,7 +69,7 @@ export default class Deploy extends Command { description: 'Seconds to wait for a conflicting lock to release (0 = fail immediately)', default: 0, }), - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/commands/import.ts b/src/commands/import.ts index 6bd94df..b87d0c9 100644 --- a/src/commands/import.ts +++ b/src/commands/import.ts @@ -2,6 +2,7 @@ import {Command, Args, Flags} from '@oclif/core' import type {components} from '../lib/api.generated.js' import {createApiClient} from '../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../lib/auth.js' +import {urlFlag} from '../lib/validators.js' import {fetchAllRefs} from '../lib/yaml/resolver.js' import {allHandlers} from '../lib/yaml/handlers.js' import {fetchPaginated} from '../lib/typed-api.js' @@ -37,7 +38,7 @@ export default class Import extends Command { } static flags = { - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/commands/incidents/resolve.ts b/src/commands/incidents/resolve.ts index 6e0d516..d460dfe 100644 --- a/src/commands/incidents/resolve.ts +++ b/src/commands/incidents/resolve.ts @@ -1,11 +1,12 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class IncidentsResolve extends Command { static description = 'Resolve an incident' static examples = ['<%= config.bin %> incidents resolve 42'] - static args = {id: Args.string({description: 'Incident ID', required: true})} + static args = {id: uuidArg({description: 'Incident ID', required: true})} static flags = { ...globalFlags, message: Flags.string({description: 'Resolution message'}), diff --git a/src/commands/monitors/pause.ts b/src/commands/monitors/pause.ts index e54677b..7e910b3 100644 --- a/src/commands/monitors/pause.ts +++ b/src/commands/monitors/pause.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class MonitorsPause extends Command { static description = 'Pause a monitor' static examples = ['<%= config.bin %> monitors pause 42'] - static args = {id: Args.string({description: 'Monitor ID', required: true})} + static args = {id: uuidArg({description: 'Monitor ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/monitors/results.ts b/src/commands/monitors/results.ts index 67587e0..8952df1 100644 --- a/src/commands/monitors/results.ts +++ b/src/commands/monitors/results.ts @@ -1,14 +1,15 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' import {fetchCursorPaginated} from '../../lib/typed-api.js' import type {components} from '../../lib/api.generated.js' +import {uuidArg} from '../../lib/validators.js' type CheckResultDto = components['schemas']['CheckResultDto'] export default class MonitorsResults extends Command { static description = 'Show recent check results for a monitor' static examples = ['<%= config.bin %> monitors results 42'] - static args = {id: Args.string({description: 'Monitor ID', required: true})} + static args = {id: uuidArg({description: 'Monitor ID', required: true})} static flags = { ...globalFlags, limit: Flags.integer({description: 'Maximum number of results to show (1–1000)', default: 20}), @@ -23,12 +24,12 @@ export default class MonitorsResults extends Command { {maxItems: flags.limit}, ) display(this, items, flags.output, [ - {header: 'ID', get: (r) => String(r.id ?? '')}, - {header: 'PASSED', get: (r) => (r.passed == null ? '' : r.passed ? 'Pass' : 'Fail')}, - {header: 'RESPONSE TIME', get: (r) => (r.responseTimeMs != null ? `${r.responseTimeMs}ms` : '')}, - {header: 'CODE', get: (r) => String(r.details?.statusCode ?? '')}, - {header: 'REGION', get: (r) => String(r.region ?? '')}, - {header: 'TIMESTAMP', get: (r) => String(r.timestamp ?? '')}, + {header: 'ID', get: (r: CheckResultDto) => String(r.id ?? '')}, + {header: 'PASSED', get: (r: CheckResultDto) => (r.passed == null ? '' : r.passed ? 'Pass' : 'Fail')}, + {header: 'RESPONSE TIME', get: (r: CheckResultDto) => (r.responseTimeMs != null ? `${r.responseTimeMs}ms` : '')}, + {header: 'CODE', get: (r: CheckResultDto) => String(r.details?.statusCode ?? '')}, + {header: 'REGION', get: (r: CheckResultDto) => String(r.region ?? '')}, + {header: 'TIMESTAMP', get: (r: CheckResultDto) => String(r.timestamp ?? '')}, ]) } } diff --git a/src/commands/monitors/resume.ts b/src/commands/monitors/resume.ts index c84bfa6..8e0088a 100644 --- a/src/commands/monitors/resume.ts +++ b/src/commands/monitors/resume.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class MonitorsResume extends Command { static description = 'Resume a paused monitor' static examples = ['<%= config.bin %> monitors resume 42'] - static args = {id: Args.string({description: 'Monitor ID', required: true})} + static args = {id: uuidArg({description: 'Monitor ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/monitors/test.ts b/src/commands/monitors/test.ts index 2d37af1..49474ee 100644 --- a/src/commands/monitors/test.ts +++ b/src/commands/monitors/test.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class MonitorsTest extends Command { static description = 'Run an ad-hoc test for a monitor' static examples = ['<%= config.bin %> monitors test 42'] - static args = {id: Args.string({description: 'Monitor ID', required: true})} + static args = {id: uuidArg({description: 'Monitor ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/monitors/versions/get.ts b/src/commands/monitors/versions/get.ts index dbe7b11..149cbd0 100644 --- a/src/commands/monitors/versions/get.ts +++ b/src/commands/monitors/versions/get.ts @@ -2,6 +2,7 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiGet} from '../../../lib/api-client.js' import type {components} from '../../../lib/api.generated.js' +import {uuidArg} from '../../../lib/validators.js' type MonitorVersionDto = components['schemas']['MonitorVersionDto'] @@ -13,7 +14,7 @@ export default class MonitorsVersionsGet extends Command { ] static args = { - id: Args.string({description: 'Monitor ID', required: true}), + id: uuidArg({description: 'Monitor ID', required: true}), version: Args.integer({description: 'Version number', required: true}), } diff --git a/src/commands/monitors/versions/list.ts b/src/commands/monitors/versions/list.ts index 54ac671..6250f41 100644 --- a/src/commands/monitors/versions/list.ts +++ b/src/commands/monitors/versions/list.ts @@ -1,7 +1,8 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' import type {components} from '../../../lib/api.generated.js' +import {uuidArg} from '../../../lib/validators.js' type MonitorVersionDto = components['schemas']['MonitorVersionDto'] @@ -13,7 +14,7 @@ export default class MonitorsVersionsList extends Command { '<%= config.bin %> monitors versions list 42 -o json', ] - static args = {id: Args.string({description: 'Monitor ID', required: true})} + static args = {id: uuidArg({description: 'Monitor ID', required: true})} static flags = { ...globalFlags, limit: Flags.integer({description: 'Maximum number of versions to show', default: 20}), @@ -29,11 +30,11 @@ export default class MonitorsVersionsList extends Command { ) const items = allVersions.slice(0, flags.limit) display(this, items, flags.output, [ - {header: 'VERSION', get: (r) => String(r.version ?? '')}, - {header: 'CHANGED VIA', get: (r) => String(r.changedVia ?? '')}, - {header: 'SUMMARY', get: (r) => r.changeSummary ?? ''}, - {header: 'CREATED AT', get: (r) => String(r.createdAt ?? '')}, - {header: 'ID', get: (r) => String(r.id ?? '')}, + {header: 'VERSION', get: (r: MonitorVersionDto) => String(r.version ?? '')}, + {header: 'CHANGED VIA', get: (r: MonitorVersionDto) => String(r.changedVia ?? '')}, + {header: 'SUMMARY', get: (r: MonitorVersionDto) => r.changeSummary ?? ''}, + {header: 'CREATED AT', get: (r: MonitorVersionDto) => String(r.createdAt ?? '')}, + {header: 'ID', get: (r: MonitorVersionDto) => String(r.id ?? '')}, ]) } } diff --git a/src/commands/notification-policies/test.ts b/src/commands/notification-policies/test.ts index 3c434c1..b397174 100644 --- a/src/commands/notification-policies/test.ts +++ b/src/commands/notification-policies/test.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class NotificationPoliciesTest extends Command { static description = 'Test a notification policy' static examples = ['<%= config.bin %> notification-policies test '] - static args = {id: Args.string({description: 'Policy ID', required: true})} + static args = {id: uuidArg({description: 'Policy ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/plan.ts b/src/commands/plan.ts index e3ca9fd..feabd51 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -2,6 +2,7 @@ import {Command, Flags} from '@oclif/core' import {createApiClient} from '../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../lib/auth.js' import {EXIT_CODES} from '../lib/errors.js' +import {urlFlag} from '../lib/validators.js' import {loadConfig, validate, validatePlanRefs, fetchAllRefs, registerYamlPendingRefs, diff, formatPlan, changesetToJson, readState, emptyState, previewMovedBlocks, StateFileCorruptError} from '../lib/yaml/index.js' import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' @@ -42,7 +43,7 @@ export default class Plan extends Command { options: ['text', 'json'], default: 'text', }), - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/commands/state/pull.ts b/src/commands/state/pull.ts index e885bb5..cdd1891 100644 --- a/src/commands/state/pull.ts +++ b/src/commands/state/pull.ts @@ -2,6 +2,7 @@ import {Command, Flags} from '@oclif/core' import type {components} from '../../lib/api.generated.js' import {createApiClient} from '../../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../../lib/auth.js' +import {urlFlag} from '../../lib/validators.js' import {fetchAllRefs} from '../../lib/yaml/resolver.js' import {allHandlers} from '../../lib/yaml/handlers.js' import {fetchPaginated} from '../../lib/typed-api.js' @@ -20,7 +21,7 @@ export default class StatePull extends Command { static flags = { 'dry-run': Flags.boolean({description: 'Show what would be written without saving', default: false}), - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/commands/status-pages/components/create.ts b/src/commands/status-pages/components/create.ts index 96da68b..a3f613d 100644 --- a/src/commands/status-pages/components/create.ts +++ b/src/commands/status-pages/components/create.ts @@ -1,12 +1,13 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' import {STATUS_PAGE_COMPONENT_TYPES} from '../../../lib/spec-facts.generated.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesComponentsCreate extends Command { static description = 'Add a component to a status page' static examples = ['<%= config.bin %> status-pages components create --name "API" --type STATIC'] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, name: Flags.string({description: 'Component name', required: true}), diff --git a/src/commands/status-pages/components/delete.ts b/src/commands/status-pages/components/delete.ts index 3e8ecee..7d10c59 100644 --- a/src/commands/status-pages/components/delete.ts +++ b/src/commands/status-pages/components/delete.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiDelete} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesComponentsDelete extends Command { static description = 'Remove a component from a status page' static examples = ['<%= config.bin %> status-pages components delete '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'component-id': Args.string({description: 'Component ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'component-id': uuidArg({description: 'Component ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/components/list.ts b/src/commands/status-pages/components/list.ts index 72c53e2..ae8fc1d 100644 --- a/src/commands/status-pages/components/list.ts +++ b/src/commands/status-pages/components/list.ts @@ -1,14 +1,15 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' +import {uuidArg} from '../../../lib/validators.js' type StatusPageComponent = components['schemas']['StatusPageComponentDto'] export default class StatusPagesComponentsList extends Command { static description = 'List components on a status page' static examples = ['<%= config.bin %> status-pages components list '] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/status-pages/components/update.ts b/src/commands/status-pages/components/update.ts index 1c32c4b..a6430d1 100644 --- a/src/commands/status-pages/components/update.ts +++ b/src/commands/status-pages/components/update.ts @@ -1,13 +1,14 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPut} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesComponentsUpdate extends Command { static description = 'Update a status page component' static examples = ['<%= config.bin %> status-pages components update --name "API v2"'] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'component-id': Args.string({description: 'Component ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'component-id': uuidArg({description: 'Component ID', required: true}), } static flags = { ...globalFlags, diff --git a/src/commands/status-pages/domains/add.ts b/src/commands/status-pages/domains/add.ts index c0bc9eb..73d8f55 100644 --- a/src/commands/status-pages/domains/add.ts +++ b/src/commands/status-pages/domains/add.ts @@ -1,11 +1,12 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' 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 args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, hostname: Flags.string({description: 'Custom domain hostname', required: true}), diff --git a/src/commands/status-pages/domains/list.ts b/src/commands/status-pages/domains/list.ts index 4c3118f..4b8c104 100644 --- a/src/commands/status-pages/domains/list.ts +++ b/src/commands/status-pages/domains/list.ts @@ -1,14 +1,15 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' +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 examples = ['<%= config.bin %> status-pages domains list '] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/status-pages/domains/remove.ts b/src/commands/status-pages/domains/remove.ts index c161383..4133310 100644 --- a/src/commands/status-pages/domains/remove.ts +++ b/src/commands/status-pages/domains/remove.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiDelete} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesDomainsRemove extends Command { static description = 'Remove a custom domain from a status page' static examples = ['<%= config.bin %> status-pages domains remove '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'domain-id': Args.string({description: 'Domain ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'domain-id': uuidArg({description: 'Domain ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/domains/verify.ts b/src/commands/status-pages/domains/verify.ts index 4d14293..ff891cf 100644 --- a/src/commands/status-pages/domains/verify.ts +++ b/src/commands/status-pages/domains/verify.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesDomainsVerify extends Command { static description = 'Verify a custom domain on a status page' static examples = ['<%= config.bin %> status-pages domains verify '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'domain-id': Args.string({description: 'Domain ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'domain-id': uuidArg({description: 'Domain ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/groups/create.ts b/src/commands/status-pages/groups/create.ts index 7491728..e1dc121 100644 --- a/src/commands/status-pages/groups/create.ts +++ b/src/commands/status-pages/groups/create.ts @@ -1,11 +1,12 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesGroupsCreate extends Command { static description = 'Create a component group on a status page' static examples = ['<%= config.bin %> status-pages groups create --name "Infrastructure"'] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, name: Flags.string({description: 'Group name', required: true}), diff --git a/src/commands/status-pages/groups/delete.ts b/src/commands/status-pages/groups/delete.ts index e5d8ff4..162204e 100644 --- a/src/commands/status-pages/groups/delete.ts +++ b/src/commands/status-pages/groups/delete.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiDelete} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesGroupsDelete extends Command { static description = 'Delete a component group from a status page' static examples = ['<%= config.bin %> status-pages groups delete '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'group-id': Args.string({description: 'Group ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'group-id': uuidArg({description: 'Group ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/groups/list.ts b/src/commands/status-pages/groups/list.ts index 8a9048e..4399174 100644 --- a/src/commands/status-pages/groups/list.ts +++ b/src/commands/status-pages/groups/list.ts @@ -1,14 +1,15 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' +import {uuidArg} from '../../../lib/validators.js' type StatusPageComponentGroup = components['schemas']['StatusPageComponentGroupDto'] export default class StatusPagesGroupsList extends Command { static description = 'List component groups on a status page' static examples = ['<%= config.bin %> status-pages groups list '] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/commands/status-pages/groups/update.ts b/src/commands/status-pages/groups/update.ts index e32b70a..0216ffb 100644 --- a/src/commands/status-pages/groups/update.ts +++ b/src/commands/status-pages/groups/update.ts @@ -1,13 +1,14 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPut} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesGroupsUpdate extends Command { static description = 'Update a component group on a status page' static examples = ['<%= config.bin %> status-pages groups update --name "Core"'] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'group-id': Args.string({description: 'Group ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'group-id': uuidArg({description: 'Group ID', required: true}), } static flags = { ...globalFlags, diff --git a/src/commands/status-pages/incidents/create.ts b/src/commands/status-pages/incidents/create.ts index 0a6d4bf..295e533 100644 --- a/src/commands/status-pages/incidents/create.ts +++ b/src/commands/status-pages/incidents/create.ts @@ -1,12 +1,13 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' import {SP_INCIDENT_IMPACTS, SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsCreate extends Command { static description = 'Create an incident on a status page' static examples = ['<%= config.bin %> status-pages incidents create --title "Outage" --impact MAJOR'] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, title: Flags.string({description: 'Incident title', required: true}), diff --git a/src/commands/status-pages/incidents/delete.ts b/src/commands/status-pages/incidents/delete.ts index 79c2af6..46f6fea 100644 --- a/src/commands/status-pages/incidents/delete.ts +++ b/src/commands/status-pages/incidents/delete.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiDelete} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsDelete extends Command { static description = 'Delete a status page incident' static examples = ['<%= config.bin %> status-pages incidents delete '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/incidents/dismiss.ts b/src/commands/status-pages/incidents/dismiss.ts index 9312c57..03edc16 100644 --- a/src/commands/status-pages/incidents/dismiss.ts +++ b/src/commands/status-pages/incidents/dismiss.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsDismiss extends Command { static description = 'Dismiss a draft status page incident' static examples = ['<%= config.bin %> status-pages incidents dismiss '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/incidents/get.ts b/src/commands/status-pages/incidents/get.ts index 5bae741..cfb7016 100644 --- a/src/commands/status-pages/incidents/get.ts +++ b/src/commands/status-pages/incidents/get.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiGet} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsGet extends Command { static description = 'Get a status page incident with timeline' static examples = ['<%= config.bin %> status-pages incidents get '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/incidents/list.ts b/src/commands/status-pages/incidents/list.ts index 7ad023d..2236a60 100644 --- a/src/commands/status-pages/incidents/list.ts +++ b/src/commands/status-pages/incidents/list.ts @@ -1,14 +1,15 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' +import {uuidArg} from '../../../lib/validators.js' type StatusPageIncident = components['schemas']['StatusPageIncidentDto'] export default class StatusPagesIncidentsList extends Command { static description = 'List incidents on a status page' static examples = ['<%= config.bin %> status-pages incidents list '] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, limit: Flags.integer({description: 'Maximum number of incidents to show', default: 20}), diff --git a/src/commands/status-pages/incidents/post-update.ts b/src/commands/status-pages/incidents/post-update.ts index 1c93601..249c5ed 100644 --- a/src/commands/status-pages/incidents/post-update.ts +++ b/src/commands/status-pages/incidents/post-update.ts @@ -1,14 +1,15 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' import {SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsPostUpdate extends Command { static description = 'Post a timeline update on a status page incident' static examples = ['<%= config.bin %> status-pages incidents post-update --body "Fix deployed" --status MONITORING'] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = { ...globalFlags, diff --git a/src/commands/status-pages/incidents/publish.ts b/src/commands/status-pages/incidents/publish.ts index fd15e4e..64b053c 100644 --- a/src/commands/status-pages/incidents/publish.ts +++ b/src/commands/status-pages/incidents/publish.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsPublish extends Command { static description = 'Publish a draft status page incident' static examples = ['<%= config.bin %> status-pages incidents publish '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/status-pages/incidents/update.ts b/src/commands/status-pages/incidents/update.ts index fe5e173..b0c3b21 100644 --- a/src/commands/status-pages/incidents/update.ts +++ b/src/commands/status-pages/incidents/update.ts @@ -1,14 +1,15 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPut} from '../../../lib/api-client.js' import {SP_INCIDENT_IMPACTS, SP_INCIDENT_STATUSES} from '../../../lib/spec-facts.generated.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesIncidentsUpdate extends Command { static description = 'Update a status page incident' static examples = ['<%= config.bin %> status-pages incidents update --status RESOLVED'] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'incident-id': Args.string({description: 'Incident ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'incident-id': uuidArg({description: 'Incident ID', required: true}), } static flags = { ...globalFlags, diff --git a/src/commands/status-pages/subscribers/add.ts b/src/commands/status-pages/subscribers/add.ts index d949437..85e000a 100644 --- a/src/commands/status-pages/subscribers/add.ts +++ b/src/commands/status-pages/subscribers/add.ts @@ -1,11 +1,12 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {apiPost} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesSubscribersAdd extends Command { static description = 'Add a subscriber to a status page' static examples = ['<%= config.bin %> status-pages subscribers add --email user@example.com'] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, email: Flags.string({description: 'Subscriber email address', required: true}), diff --git a/src/commands/status-pages/subscribers/list.ts b/src/commands/status-pages/subscribers/list.ts index 11f728d..e06252f 100644 --- a/src/commands/status-pages/subscribers/list.ts +++ b/src/commands/status-pages/subscribers/list.ts @@ -1,14 +1,15 @@ -import {Command, Args, Flags} from '@oclif/core' +import {Command, Flags} from '@oclif/core' import type {components} from '../../../lib/api.generated.js' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' import {fetchPaginated} from '../../../lib/typed-api.js' +import {uuidArg} from '../../../lib/validators.js' type StatusPageSubscriber = components['schemas']['StatusPageSubscriberDto'] export default class StatusPagesSubscribersList extends Command { static description = 'List subscribers on a status page' static examples = ['<%= config.bin %> status-pages subscribers list '] - static args = {id: Args.string({description: 'Status page ID', required: true})} + static args = {id: uuidArg({description: 'Status page ID', required: true})} static flags = { ...globalFlags, limit: Flags.integer({description: 'Maximum number of subscribers to show', default: 20}), diff --git a/src/commands/status-pages/subscribers/remove.ts b/src/commands/status-pages/subscribers/remove.ts index 3a9b3ec..1f6b50b 100644 --- a/src/commands/status-pages/subscribers/remove.ts +++ b/src/commands/status-pages/subscribers/remove.ts @@ -1,13 +1,14 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../../lib/base-command.js' import {apiDelete} from '../../../lib/api-client.js' +import {uuidArg} from '../../../lib/validators.js' export default class StatusPagesSubscribersRemove extends Command { static description = 'Remove a subscriber from a status page' static examples = ['<%= config.bin %> status-pages subscribers remove '] static args = { - id: Args.string({description: 'Status page ID', required: true}), - 'subscriber-id': Args.string({description: 'Subscriber ID', required: true}), + id: uuidArg({description: 'Status page ID', required: true}), + 'subscriber-id': uuidArg({description: 'Subscriber ID', required: true}), } static flags = {...globalFlags} diff --git a/src/commands/version.ts b/src/commands/version.ts index b3ea7b2..283415c 100644 --- a/src/commands/version.ts +++ b/src/commands/version.ts @@ -10,7 +10,7 @@ export default class Version extends Command { async run(): Promise { await this.parse(Version) const __dirname = dirname(fileURLToPath(import.meta.url)) - const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')) + const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8')) as {version: string} this.log(`devhelm/${pkg.version} ${process.platform}-${process.arch} node-${process.version}`) } } diff --git a/src/commands/webhooks/test.ts b/src/commands/webhooks/test.ts index 2474361..adee1db 100644 --- a/src/commands/webhooks/test.ts +++ b/src/commands/webhooks/test.ts @@ -1,11 +1,12 @@ -import {Command, Args} from '@oclif/core' +import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' +import {uuidArg} from '../../lib/validators.js' export default class WebhooksTest extends Command { static description = 'Send a test event to a webhook' static examples = ['<%= config.bin %> webhooks test '] - static args = {id: Args.string({description: 'Webhook ID', required: true})} + static args = {id: uuidArg({description: 'Webhook ID', required: true})} static flags = {...globalFlags} async run() { diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 2090baf..c00fc68 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -18,8 +18,9 @@ export class ApiRequestError extends Error { private static parseBody(body: string): string { try { - const json = JSON.parse(body) - return json.message || json.error || body + const json = JSON.parse(body) as Record + const msg = json.message ?? json.error + return typeof msg === 'string' ? msg : body } catch { return body || 'Unknown API error' } diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index ca9fa6a..d81a850 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -3,6 +3,7 @@ import {createApiClient, type ApiClient} from './api-client.js' import {resolveToken, resolveApiUrl} from './auth.js' import {AuthError} from './errors.js' import {formatOutput, OutputFormat, ColumnDef} from './output.js' +import {urlFlag} from './validators.js' export const globalFlags = { output: Flags.string({ @@ -11,7 +12,7 @@ export const globalFlags = { options: ['table', 'json', 'yaml'], default: 'table', }), - 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-url': urlFlag({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), } diff --git a/src/lib/crud-commands.ts b/src/lib/crud-commands.ts index c91aff1..3bf9894 100644 --- a/src/lib/crud-commands.ts +++ b/src/lib/crud-commands.ts @@ -3,12 +3,18 @@ import {globalFlags, buildClient, display} from './base-command.js' import {fetchPaginated} from './typed-api.js' import {apiGet, apiPost, apiPut, apiDelete} from './api-client.js' import type {ColumnDef} from './output.js' +import {uuidArg} from './validators.js' + +type Arg = Interfaces.Arg export interface ResourceConfig { name: string plural: string apiPath: string + /** Field name used as the resource identifier (default: 'id'). */ idField?: string + /** Set to false to skip UUID validation on the id arg (e.g. for slug/key ids). */ + validateIdAsUuid?: boolean columns: ColumnDef[] createFlags?: Interfaces.FlagInput updateFlags?: Interfaces.FlagInput @@ -36,12 +42,19 @@ export function createListCommand(config: ResourceConfig) { return ListCmd } +function idArg(config: Pick): Arg { + const idLabel = config.idField ?? 'id' + const useUuid = config.validateIdAsUuid ?? (idLabel === 'id' || idLabel === 'subscriptionId') + if (useUuid) return uuidArg({description: `${config.name} ${idLabel}`, required: true}) + return Args.string({description: `${config.name} ${idLabel}`, required: true}) +} + export function createGetCommand(config: ResourceConfig) { const idLabel = config.idField ?? 'id' class GetCmd extends Command { static description = `Get a ${config.name} by ${idLabel}` static examples = [`<%= config.bin %> ${config.plural} get <${idLabel}>`] - static args = {[idLabel]: Args.string({description: `${config.name} ${idLabel}`, required: true})} + static args = {[idLabel]: idArg(config)} static flags = {...globalFlags} async run() { @@ -82,7 +95,7 @@ export function createUpdateCommand(config: ResourceConfig) { class UpdateCmd extends Command { static description = `Update a ${config.name}` static examples = [`<%= config.bin %> ${config.plural} update <${idLabel}>`] - static args = {[idLabel]: Args.string({description: `${config.name} ${idLabel}`, required: true})} + static args = {[idLabel]: idArg(config)} static flags = {...globalFlags, ...resourceFlags} async run() { @@ -105,7 +118,7 @@ export function createDeleteCommand(config: ResourceConfig) { class DeleteCmd extends Command { static description = `Delete a ${config.name}` static examples = [`<%= config.bin %> ${config.plural} delete <${idLabel}>`] - static args = {[idLabel]: Args.string({description: `${config.name} ${idLabel}`, required: true})} + static args = {[idLabel]: idArg(config)} static flags = {...globalFlags} async run() { diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 9518ef9..8888eb8 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -1,8 +1,11 @@ import {readFileSync} from 'node:fs' import {Flags} from '@oclif/core' +import {z} from 'zod' import {ResourceConfig} from './crud-commands.js' import type {components} from './api.generated.js' +import {schemas as apiSchemas} from './api-zod.generated.js' import {fieldDescriptions} from './descriptions.generated.js' +import {urlFlag} from './validators.js' import { MONITOR_TYPES, HTTP_METHODS, @@ -182,18 +185,18 @@ export const ALERT_CHANNELS: ResourceConfig = { options: [...CHANNEL_TYPES], }), config: Flags.string({description: 'Channel-specific configuration as JSON'}), - 'webhook-url': Flags.string({description: desc('SlackChannelConfig', 'webhookUrl', 'Slack/Discord/Teams webhook URL')}), + 'webhook-url': urlFlag({description: desc('SlackChannelConfig', 'webhookUrl', 'Slack/Discord/Teams webhook URL')}), }, updateFlags: { name: Flags.string({description: desc('UpdateAlertChannelRequest', 'name')}), type: Flags.string({description: 'Alert channel type', options: [...CHANNEL_TYPES]}), config: Flags.string({description: 'Channel-specific configuration as JSON'}), - 'webhook-url': Flags.string({description: desc('SlackChannelConfig', 'webhookUrl', 'Slack/Discord/Teams webhook URL')}), + 'webhook-url': urlFlag({description: desc('SlackChannelConfig', 'webhookUrl', 'Slack/Discord/Teams webhook URL')}), }, bodyBuilder: (raw) => { let config: CreateAlertChannelRequest['config'] | undefined if (raw.config) { - config = JSON.parse(String(raw.config)) as CreateAlertChannelRequest['config'] + config = parseAlertChannelConfigFlag(String(raw.config)) } else { const channelType = String(raw.type || 'slack').toLowerCase() if (raw['webhook-url'] !== undefined) { @@ -209,6 +212,59 @@ export const ALERT_CHANNELS: ResourceConfig = { }, } +// Validates a `--config` JSON string against the per-channelType schema from +// the OpenAPI spec. Throws with a clear message if the JSON is malformed, +// the discriminator is missing, or the payload doesn't match the expected +// shape — so users see the error here rather than a generic API 400. +function parseAlertChannelConfigFlag(raw: string): CreateAlertChannelRequest['config'] { + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + throw new Error(`Failed to parse --config as JSON: ${msg}`) + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('--config must be a JSON object, e.g. \'{"channelType":"slack","webhookUrl":"..."}\'') + } + + const channelType = (parsed as Record).channelType + if (typeof channelType !== 'string') { + throw new Error( + `--config must include "channelType" (one of: ${[...CHANNEL_TYPES].join(', ')})`, + ) + } + + const schema = ALERT_CHANNEL_CONFIG_SCHEMAS[channelType] + if (!schema) { + throw new Error( + `Unknown channelType "${channelType}". Valid types: ${[...CHANNEL_TYPES].join(', ')}`, + ) + } + + const result = schema.safeParse(parsed) + if (!result.success) { + const issues = result.error.issues + .map((i) => `${i.path.join('.') || ''}: ${i.message}`) + .join('; ') + throw new Error(`Invalid --config payload for channelType "${channelType}": ${issues}`) + } + + return result.data as CreateAlertChannelRequest['config'] +} + +// Discriminated by `channelType` to match the API's AlertChannelConfig union. +const ALERT_CHANNEL_CONFIG_SCHEMAS: Record = { + discord: apiSchemas.DiscordChannelConfig, + email: apiSchemas.EmailChannelConfig, + opsgenie: apiSchemas.OpsGenieChannelConfig, + pagerduty: apiSchemas.PagerDutyChannelConfig, + slack: apiSchemas.SlackChannelConfig, + teams: apiSchemas.TeamsChannelConfig, + webhook: apiSchemas.WebhookChannelConfig, +} + export const NOTIFICATION_POLICIES: ResourceConfig = { name: 'notification policy', plural: 'notification-policies', @@ -344,12 +400,12 @@ export const WEBHOOKS: ResourceConfig = { {header: 'EVENTS', get: (r) => (r.subscribedEvents ?? []).join(', ')}, ], createFlags: { - url: Flags.string({description: desc('CreateWebhookEndpointRequest', 'url'), required: true}), + url: urlFlag({description: desc('CreateWebhookEndpointRequest', 'url'), required: true}), events: Flags.string({description: desc('CreateWebhookEndpointRequest', 'subscribedEvents'), required: true}), description: Flags.string({description: desc('CreateWebhookEndpointRequest', 'description')}), }, updateFlags: { - url: Flags.string({description: desc('UpdateWebhookEndpointRequest', 'url')}), + url: urlFlag({description: desc('UpdateWebhookEndpointRequest', 'url')}), events: Flags.string({description: desc('UpdateWebhookEndpointRequest', 'subscribedEvents')}), description: Flags.string({description: desc('UpdateWebhookEndpointRequest', 'description')}), }, @@ -368,6 +424,7 @@ export const API_KEYS: ResourceConfig = { name: 'API key', plural: 'api-keys', apiPath: '/api/v1/api-keys', + validateIdAsUuid: false, columns: [ {header: 'ID', get: (r) => String(r.id ?? '')}, {header: 'NAME', get: (r) => r.name ?? ''}, @@ -452,12 +509,44 @@ export const STATUS_PAGES: ResourceConfig = { // is `"type": "module"` and CommonJS `require` is undefined in that context. // `readFileSync` is in the always-resolved Node core, so the import cost is // effectively zero — it's already loaded before the CLI's top-level code runs. -function loadBrandingFile(path: string): unknown { +function loadBrandingFile(path: string): components['schemas']['StatusPageBranding'] { const raw = readFileSync(path, 'utf8') + let parsed: unknown try { - return JSON.parse(raw) + parsed = JSON.parse(raw) } catch (err) { const msg = err instanceof Error ? err.message : String(err) throw new Error(`Failed to parse branding file "${path}" as JSON: ${msg}`) } + + const result = BrandingFileSchema.safeParse(parsed) + if (!result.success) { + const issues = result.error.issues + .map((i) => `${i.path.join('.') || ''}: ${i.message}`) + .join('; ') + throw new Error(`Invalid branding file "${path}": ${issues}`) + } + return result.data as components['schemas']['StatusPageBranding'] } + +// Mirrors the API's StatusPageBranding record (see api-zod.generated.ts). +// Hand-defined here (rather than reusing the generated schema) so we can +// surface clearer per-field validation errors on hex colors and URLs before +// the request hits the API. +const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/ +const HTTP_URL_RE = /^https?:\/\/.+/ +const BrandingFileSchema = z.object({ + logoUrl: z.string().regex(HTTP_URL_RE, 'must be an http(s) URL').max(2048).optional(), + faviconUrl: z.string().regex(HTTP_URL_RE, 'must be an http(s) URL').max(2048).optional(), + brandColor: z.string().regex(HEX_COLOR_RE, 'must be a hex color, e.g. #4F46E5').optional(), + pageBackground: z.string().regex(HEX_COLOR_RE, 'must be a hex color').optional(), + cardBackground: z.string().regex(HEX_COLOR_RE, 'must be a hex color').optional(), + textColor: z.string().regex(HEX_COLOR_RE, 'must be a hex color').optional(), + borderColor: z.string().regex(HEX_COLOR_RE, 'must be a hex color').optional(), + headerStyle: z.string().max(50).optional(), + theme: z.string().max(50).optional(), + reportUrl: z.string().regex(HTTP_URL_RE, 'must be an http(s) URL').max(2048).optional(), + hidePoweredBy: z.boolean().optional(), + customCss: z.string().max(50_000).optional(), + customHeadHtml: z.string().max(50_000).optional(), +}).strict() diff --git a/src/lib/validators.ts b/src/lib/validators.ts new file mode 100644 index 0000000..56d1baf --- /dev/null +++ b/src/lib/validators.ts @@ -0,0 +1,38 @@ +import {Args, Flags} from '@oclif/core' + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +async function parseUuid(input: string): Promise { + if (!UUID_RE.test(input)) { + throw new Error( + `Invalid UUID format: got '${input}', expected xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, + ) + } + return input +} + +async function parseUrl(input: string): Promise { + try { + const url = new URL(input) + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('not HTTP(S)') + } + } catch { + throw new Error( + `Invalid URL: got '${input}', expected a valid HTTP(S) URL`, + ) + } + return input +} + +export function uuidArg(options: {description: string; required?: true}) { + return Args.string({...options, required: true as const, parse: parseUuid}) +} + +export function uuidFlag(options: {description: string; required?: boolean}) { + return Flags.string({...options, parse: parseUuid}) +} + +export function urlFlag(options: {description: string; required?: boolean}) { + return Flags.string({...options, parse: parseUrl}) +} diff --git a/src/lib/yaml/handlers.ts b/src/lib/yaml/handlers.ts index 01670b9..496ba51 100644 --- a/src/lib/yaml/handlers.ts +++ b/src/lib/yaml/handlers.ts @@ -214,6 +214,11 @@ function defineHandler( applyUpdate: h.applyUpdate, deletePath: h.deletePath, } + // SAFETY: ResourceHandler → ResourceHandler. + // Method params are contravariant, so TS rejects the direct assignment. + // defineHandler already verified all field accesses at compile time; + // the registry only calls methods with the correct concrete types + // (routed through HANDLER_MAP keyed by HandledResourceType). return handler as unknown as ResourceHandler } @@ -247,6 +252,10 @@ function sortedIds(ids: string[]): string[] { function stripNullish(value: T): T { if (value === null || value === undefined) return value if (Array.isArray(value)) { + // SAFETY: T is narrowed to an array type by the guard above, but TS + // can't re-narrow the generic. The mapped array preserves the runtime + // shape, so the cast back to T is sound. + // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Array.isArray narrows to any[]; cast is verified by the guard return value.map((v) => stripNullish(v)) as unknown as T } if (typeof value === 'object') { @@ -648,6 +657,10 @@ const monitorHandler = defineHandler(obj: T, env: Record = process.env): T { + return walkAndInterpolate(obj, env) as T +} + +function walkAndInterpolate(node: unknown, env: Record): unknown { + if (typeof node === 'string') { + return interpolate(node, env) + } + + if (Array.isArray(node)) { + return node.map((item) => walkAndInterpolate(item, env)) + } + + if (node !== null && typeof node === 'object') { + const result: Record = {} + for (const [key, value] of Object.entries(node as Record)) { + result[key] = walkAndInterpolate(value, env) + } + return result + } + + return node +} + /** * Find all ${VAR} references in a string without resolving them. * Returns variable names (without fallback info). @@ -81,6 +113,15 @@ export function findVariables(input: string): string[] { return vars } +/** + * Find all ${VAR} references in a parsed object's string values. + */ +export function findVariablesInObject(obj: unknown): string[] { + const vars: string[] = [] + walkStrings(obj, (s) => vars.push(...findVariables(s))) + return vars +} + /** * Check which variables would fail during interpolation (no value, no default). * Returns array of missing variable names. @@ -102,3 +143,33 @@ export function findMissingVariables(input: string, env: Record = process.env, +): string[] { + const missing: string[] = [] + walkStrings(obj, (s) => missing.push(...findMissingVariables(s, env))) + return missing +} + +function walkStrings(node: unknown, visitor: (s: string) => void): void { + if (typeof node === 'string') { + visitor(node) + return + } + + if (Array.isArray(node)) { + for (const item of node) walkStrings(item, visitor) + return + } + + if (node !== null && typeof node === 'object') { + for (const value of Object.values(node as Record)) { + walkStrings(value, visitor) + } + } +} diff --git a/src/lib/yaml/parser.ts b/src/lib/yaml/parser.ts index 8a84c30..5d0ef95 100644 --- a/src/lib/yaml/parser.ts +++ b/src/lib/yaml/parser.ts @@ -10,7 +10,7 @@ import {parse as parseYaml} from 'yaml' import type {DevhelmConfig, YamlMonitor, YamlMonitorDefaults} from './schema.js' import {YAML_SECTION_KEYS} from './schema.js' -import {interpolate, findMissingVariables} from './interpolation.js' +import {interpolateObject, findMissingVariablesInObject} from './interpolation.js' import {DevhelmConfigSchema, formatZodErrors} from './zod-schemas.js' export class ParseError extends Error { @@ -31,24 +31,9 @@ export function parseConfigFile(filePath: string, resolveEnv = true): DevhelmCon const raw = readFileSync(absPath, 'utf8') - let interpolated: string - if (resolveEnv) { - const missing = findMissingVariables(raw) - if (missing.length > 0) { - throw new ParseError( - `Missing required environment variables: ${missing.join(', ')}. ` + - 'Set them or use ${VAR:-default} syntax for fallbacks.', - filePath, - ) - } - interpolated = interpolate(raw) - } else { - interpolated = raw - } - let parsed: unknown try { - parsed = parseYaml(interpolated) + parsed = parseYaml(raw) } catch (err) { throw new ParseError(`Invalid YAML: ${err instanceof Error ? err.message : String(err)}`, filePath) } @@ -57,6 +42,18 @@ export function parseConfigFile(filePath: string, resolveEnv = true): DevhelmCon throw new ParseError('Config file is empty or not a YAML object', filePath) } + if (resolveEnv) { + const missing = findMissingVariablesInObject(parsed) + if (missing.length > 0) { + throw new ParseError( + `Missing required environment variables: ${missing.join(', ')}. ` + + 'Set them or use ${VAR:-default} syntax for fallbacks.', + filePath, + ) + } + parsed = interpolateObject(parsed) + } + const result = DevhelmConfigSchema.safeParse(parsed) if (!result.success) { const messages = formatZodErrors(result.error) diff --git a/src/lib/yaml/resolver.ts b/src/lib/yaml/resolver.ts index 470afa6..c7a5ddf 100644 --- a/src/lib/yaml/resolver.ts +++ b/src/lib/yaml/resolver.ts @@ -224,6 +224,10 @@ export async function fetchAllRefs(client: ApiClient, state?: DeployState): Prom */ export function registerYamlPendingRefs(refs: ResolvedRefs, config: DevhelmConfig): void { for (const handler of allHandlers()) { + // SAFETY: handler.configKey is a YamlSectionKey, so config[key] is one + // of the typed arrays (YamlTag[] | YamlMonitor[] | ...). The handler's + // getRefKey is type-erased to accept `unknown` in the registry, matching + // the element type. We widen to unknown[] to iterate generically. const items = config[handler.configKey] as unknown[] | undefined if (!items) continue for (const item of items) { diff --git a/src/lib/yaml/state.ts b/src/lib/yaml/state.ts index 03a9978..ca528ac 100644 --- a/src/lib/yaml/state.ts +++ b/src/lib/yaml/state.ts @@ -10,6 +10,7 @@ */ import {existsSync, readFileSync, writeFileSync, mkdirSync} from 'node:fs' import {join, dirname} from 'node:path' +import {z} from 'zod' import type {ResourceType} from './types.js' // ── V2 types ───────────────────────────────────────────────────────────── @@ -50,6 +51,40 @@ export interface DeployStateV1 { export type DeployState = DeployStateV2 +// ── Zod schemas for state file validation ──────────────────────────────── + +const ChildStateEntrySchema = z.object({ + apiId: z.string(), + attributes: z.record(z.unknown()), +}) + +const StateEntrySchema = z.object({ + apiId: z.string(), + resourceType: z.string(), + attributes: z.record(z.unknown()), + children: z.record(ChildStateEntrySchema), +}) + +const DeployStateV2Schema = z.object({ + version: z.literal('2'), + serial: z.number(), + lastDeployedAt: z.string(), + resources: z.record(StateEntrySchema), +}) + +const StateEntryV1Schema = z.object({ + resourceType: z.string(), + refKey: z.string(), + id: z.string(), + createdAt: z.string(), +}) + +const DeployStateV1Schema = z.object({ + version: z.string().optional(), + lastDeployedAt: z.string().optional(), + resources: z.array(StateEntryV1Schema), +}) + // ── Constants ──────────────────────────────────────────────────────────── const STATE_DIR = '.devhelm' @@ -164,14 +199,25 @@ export function readState(cwd: string = process.cwd()): DeployState | undefined if (raw === null || typeof raw !== 'object') { throw new StateFileCorruptError(path, new Error('expected JSON object at top level')) } + const obj = raw as Record + + // V1 detection: explicit version "1" or legacy shape (no version + array resources) if (obj.version === '1' || (obj.version === undefined && Array.isArray(obj.resources))) { - return migrateV1(obj as unknown as DeployStateV1) + const v1 = DeployStateV1Schema.safeParse(raw) + if (!v1.success) { + const issues = v1.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ') + throw new StateFileCorruptError(path, new Error(`invalid v1 state: ${issues}`)) + } + return migrateV1(v1.data as DeployStateV1) } - if (obj.version !== '2' || typeof obj.resources !== 'object' || obj.resources === null) { - throw new StateFileCorruptError(path, new Error(`unrecognized state shape (version=${String(obj.version)})`)) + + const v2 = DeployStateV2Schema.safeParse(raw) + if (!v2.success) { + const issues = v2.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ') + throw new StateFileCorruptError(path, new Error(`invalid v2 state: ${issues}`)) } - return obj as unknown as DeployState + return v2.data as DeployState } export function writeState(state: DeployState, cwd: string = process.cwd()): void { diff --git a/src/lib/yaml/transform.ts b/src/lib/yaml/transform.ts index 6e4faab..281d392 100644 --- a/src/lib/yaml/transform.ts +++ b/src/lib/yaml/transform.ts @@ -132,11 +132,17 @@ export function toCreateResourceGroupRequest( * types; we route on `monitor.type` so the type-assertion is at least * partitioned per monitor type and any shape drift surfaces as a zod * parse failure before reaching this function. + * + * SAFETY: Each YAML config type (YamlHttpConfig, etc.) is structurally + * identical to its API counterpart (HttpMonitorConfig, etc.), but + * defined in schema.ts rather than api.generated.ts, so TS treats them + * as unrelated types. Zod validation at config load time guarantees the + * shapes match before this function is reached. */ function toMonitorConfig( monitor: YamlMonitor, ): Schemas['CreateMonitorRequest']['config'] { - const cfg = monitor.config as unknown + const cfg: unknown = monitor.config switch (monitor.type) { case 'HTTP': return cfg as Schemas['HttpMonitorConfig'] case 'DNS': return cfg as Schemas['DnsMonitorConfig'] diff --git a/src/lib/yaml/zod-schemas.ts b/src/lib/yaml/zod-schemas.ts index 3c583bf..0049124 100644 --- a/src/lib/yaml/zod-schemas.ts +++ b/src/lib/yaml/zod-schemas.ts @@ -26,6 +26,29 @@ import { TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES, } from '../spec-facts.generated.js' +import {STATUS_PAGE_VISIBILITIES, MIN_FREQUENCY, MAX_FREQUENCY} from './schema.js' + +// ── Enum constants not (yet) expressed as OpenAPI enums ─────────────── +// These are the known valid values from the API source code, hardcoded +// here because the OpenAPI spec uses free-form `string` for these fields. +// Kept in sync manually — the parity test will catch drift. + +/** Match rule types supported by the notification policy engine. */ +export const MATCH_RULE_TYPES = [ + 'severity_gte', 'monitor_id_in', 'region_in', 'incident_status', + 'monitor_type_in', 'service_id_in', 'resource_group_id_in', 'component_name_in', +] as const + +/** Retry strategy kinds for resource group defaults. */ +export const RETRY_STRATEGY_TYPES = ['fixed'] as const + +/** All known webhook event type identifiers from the event catalog. */ +export const WEBHOOK_EVENT_TYPES = [ + 'monitor.created', 'monitor.updated', 'monitor.deleted', + 'incident.created', 'incident.resolved', 'incident.reopened', + 'service.status_changed', 'service.component_changed', + 'service.incident_created', 'service.incident_updated', 'service.incident_resolved', +] as const // ── Assertion config schemas (imported from generated OpenAPI Zod) ──── // Maps wire-format type strings (from AssertionConfig discriminator) @@ -105,14 +128,11 @@ const MONITOR_TYPE_CONFIG_SCHEMAS: Record = { // ── Constants ──────────────────────────────────────────────────────── // Enum tuples are imported from spec-facts.generated.ts (auto-extracted -// from the OpenAPI spec). Only STATUS_PAGE_VISIBILITIES is intentionally -// narrowed — the API also accepts PASSWORD and IP_RESTRICTED, but those -// modes are not yet wired to storage or enforcement server-side. - -const STATUS_PAGE_VISIBILITIES = ['PUBLIC'] as const - -const MIN_FREQUENCY = 30 -const MAX_FREQUENCY = 86400 +// from the OpenAPI spec). STATUS_PAGE_VISIBILITIES, MIN_FREQUENCY, and +// MAX_FREQUENCY come from schema.ts so the validator and Zod layer share +// a single source of truth — STATUS_PAGE_VISIBILITIES is intentionally +// narrowed (the spec also accepts PASSWORD and IP_RESTRICTED, but those +// modes are not yet wired to storage or enforcement server-side). export const _ZOD_ENUMS = { MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, ASSERTION_SEVERITIES, @@ -120,15 +140,19 @@ export const _ZOD_ENUMS = { TRIGGER_AGGREGATIONS, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, STATUS_PAGE_VISIBILITIES, STATUS_PAGE_INCIDENT_MODES, STATUS_PAGE_COMPONENT_TYPES, MIN_FREQUENCY, MAX_FREQUENCY, + MATCH_RULE_TYPES, RETRY_STRATEGY_TYPES, WEBHOOK_EVENT_TYPES, } as const // ── Assertion schema (dispatches by config.type to generated schemas) ─ const AssertionSchema = z.object({ - config: z.object({type: z.string()}).passthrough(), + config: z.record(z.unknown()).refine( + (c) => typeof c.type === 'string', + {message: 'config.type is required and must be a string'}, + ), severity: z.enum(ASSERTION_SEVERITIES).optional(), -}).superRefine((data, ctx) => { - const assertionType = data.config.type +}).strict().superRefine((data, ctx) => { + const assertionType = data.config.type as string const configSchema = ASSERTION_CONFIG_SCHEMAS[assertionType] if (!configSchema) { ctx.addIssue({ @@ -237,7 +261,7 @@ const EscalationChainSchema = z.object({ // ── Match rule schema ──────────────────────────────────────────────── const MatchRuleSchema = z.object({ - type: z.string(), + type: z.enum(MATCH_RULE_TYPES), value: z.string().optional(), monitorNames: z.array(z.string()).optional(), regions: z.array(z.string()).optional(), @@ -259,7 +283,7 @@ export const ChannelConfigSchema = z.union([ // ── Retry strategy schema ──────────────────────────────────────────── const RetryStrategySchema = z.object({ - type: z.string(), + type: z.enum(RETRY_STRATEGY_TYPES), maxRetries: z.number().int().positive().optional(), interval: z.number().int().positive().optional(), }).strict() @@ -285,9 +309,22 @@ const SecretSchema = z.object({ const AlertChannelSchema = z.object({ name: z.string(), - config: z.object({channelType: z.enum(CHANNEL_TYPES)}).passthrough(), -}).superRefine((data, ctx) => { - const configSchema = CHANNEL_CONFIG_SCHEMAS[data.config.channelType] + config: z.record(z.unknown()).refine( + (c) => typeof c.channelType === 'string', + {message: 'config.channelType is required'}, + ), +}).strict().superRefine((data, ctx) => { + const channelType = data.config.channelType as string + if (!(CHANNEL_TYPES as readonly string[]).includes(channelType)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['config', 'channelType'], + message: `Unknown channel type "${channelType}". Valid types: ${CHANNEL_TYPES.join(', ')}`, + }) + return + } + + const configSchema = CHANNEL_CONFIG_SCHEMAS[channelType] if (!configSchema) return const result = configSchema.safeParse(data.config) if (!result.success) { @@ -307,7 +344,7 @@ const NotificationPolicySchema = z.object({ const WebhookSchema = z.object({ url: z.string(), - subscribedEvents: z.array(z.string()).min(1), + subscribedEvents: z.array(z.enum(WEBHOOK_EVENT_TYPES)).min(1), description: z.string().optional(), enabled: z.boolean().optional(), }).strict() @@ -333,7 +370,7 @@ const ResourceGroupSchema = z.object({ const MonitorSchema = z.object({ name: z.string(), type: z.enum(MONITOR_TYPES), - config: z.object({}).passthrough(), + config: z.record(z.unknown()), frequencySeconds: z.number().int().min(MIN_FREQUENCY).max(MAX_FREQUENCY).optional(), enabled: z.boolean().optional(), regions: z.array(z.string()).optional(), diff --git a/src/types/openapi.d.ts b/src/types/openapi.d.ts deleted file mode 100644 index d4888b6..0000000 --- a/src/types/openapi.d.ts +++ /dev/null @@ -1,11402 +0,0 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - "/api/v1/alert-channels": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List active alert channels for the authenticated org */ - get: operations["list_14"]; - put?: never; - /** Create a new alert channel with encrypted config */ - post: operations["create_15"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-channels/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update an alert channel's name and re-encrypt config */ - put: operations["update_14"]; - post?: never; - /** Soft-delete an alert channel and return affected policy summary */ - delete: operations["delete_10"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-channels/{id}/deliveries": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List delivery history for an alert channel */ - get: operations["listDeliveries_1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-channels/{id}/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Test a saved alert channel's connectivity */ - post: operations["test_2"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-channels/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Test alert channel connectivity using raw config (no saved channel required) */ - post: operations["testConfig"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-deliveries/{id}/attempts": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List delivery attempts for a specific alert delivery - * @description Returns the ordered list of delivery attempts (request/response audit data) for the given delivery ID. - */ - get: operations["listAttempts"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/alert-deliveries/{id}/retry": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Retry a failed delivery - * @description Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it. - */ - post: operations["retry"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/api-keys": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List API keys */ - get: operations["list_13"]; - put?: never; - /** Create API key */ - post: operations["create_14"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/api-keys/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Delete API key */ - delete: operations["delete_11"]; - options?: never; - head?: never; - /** Update API key */ - patch: operations["update_15"]; - trace?: never; - }; - "/api/v1/api-keys/{id}/regenerate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Regenerate API key */ - post: operations["regenerate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/api-keys/{id}/revoke": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Revoke API key */ - post: operations["revoke_1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/audit-log": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List audit events for the current organization */ - get: operations["list_19"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get current API key identity - * @description Returns the authenticated API key's metadata, organization, billing plan, entitlements with usage, and current rate-limit quota. Only available for API key authentication (Bearer dh_live_...). - */ - get: operations["me_1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/categories": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List categories with service counts */ - get: operations["listCategories"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/dashboard/overview": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Dashboard overview - * @description Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute. - */ - get: operations["overview"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/deploy/lock": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get current deploy lock - * @description Returns the active deploy lock for the current workspace, if any. - */ - get: operations["current"]; - put?: never; - /** - * Acquire deploy lock - * @description Acquires an exclusive deploy lock for the current workspace. Returns 409 Conflict if the workspace is already locked by another session. - */ - post: operations["acquire"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/deploy/lock/{lockId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Release deploy lock - * @description Releases a deploy lock by ID. Only the lock holder should call this. - */ - delete: operations["release"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/deploy/lock/force": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** - * Force-release deploy lock - * @description Forcibly removes any deploy lock on the current workspace. Use to break stale locks. - */ - delete: operations["forceRelease"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/environments": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List environments */ - get: operations["list_12"]; - put?: never; - /** Create environment */ - post: operations["create_13"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/environments/{slug}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get environment by slug */ - get: operations["get_7"]; - /** Update environment */ - put: operations["update_13"]; - post?: never; - /** Delete environment */ - delete: operations["delete_9"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/heartbeat/{token}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Record a heartbeat ping (GET) - * @description Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK. - */ - get: operations["pingGet"]; - put?: never; - /** - * Record a heartbeat ping (POST) - * @description Called by external systems to signal liveness with an optional JSON payload. The payload can be inspected by heartbeat_payload_contains assertions. Always returns 200 OK. - */ - post: operations["pingPost"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/incidents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List incidents for the authenticated org */ - get: operations["list_11"]; - put?: never; - /** Create a manual incident */ - post: operations["create_12"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/incidents/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get incident details including update timeline */ - get: operations["get_10"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/incidents/{id}/resolve": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Resolve an incident */ - post: operations["resolve"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/incidents/{id}/updates": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Add an update to an incident (optionally change status) */ - post: operations["addUpdate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/integrations": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all supported integration types - * @description Returns the full static catalog of supported alert channel integration types with their metadata and config field schemas. Used by the frontend to dynamically render the 'Add Alert Channel' form. - */ - get: operations["list_18"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/invites": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List invites */ - get: operations["list_10"]; - put?: never; - /** Create invite */ - post: operations["create_11"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/invites/{inviteId}/resend": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Resend invite */ - post: operations["resend"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/invites/{inviteId}/revoke": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Revoke invite */ - post: operations["revoke"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/maintenance-windows": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List maintenance windows for the authenticated org - * @description Returns maintenance windows for the caller's organisation. Optionally filter by monitor_id, and/or by status: 'active' (currently in window) or 'upcoming' (starts in the future). - */ - get: operations["list_9"]; - put?: never; - /** - * Create a maintenance window - * @description Creates a new maintenance window. Set monitorId to null to create an org-wide window that suppresses alerts for all monitors. - */ - post: operations["create_10"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/maintenance-windows/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a single maintenance window by ID */ - get: operations["getById_2"]; - /** Update a maintenance window */ - put: operations["update_12"]; - post?: never; - /** Delete a maintenance window */ - delete: operations["delete_8"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/members": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List organization members */ - get: operations["list_17"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/members/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove member from organization */ - delete: operations["remove_2"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/members/{userId}/role": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Change member role */ - put: operations["changeRole"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/members/{userId}/status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Change member status */ - put: operations["changeStatus"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List monitors for the authenticated org */ - get: operations["list_8"]; - put?: never; - /** Create a new monitor */ - post: operations["create_9"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a single monitor by id */ - get: operations["get_6"]; - /** Update a monitor */ - put: operations["update_11"]; - post?: never; - /** Soft-delete a monitor */ - delete: operations["delete_7"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/pause": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Pause a monitor (set enabled=false) */ - post: operations["pause"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/results": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List raw check results - * @description Returns check results for the given monitor with optional time-range, region, and pass/fail filtering. Uses cursor-based pagination — pass the returned `cursor` value on subsequent requests to retrieve the next page. The cursor encodes the original time bounds, so `from`/`to` are ignored when a cursor is present. - */ - get: operations["getResults"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/results/summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get results summary - * @description Returns a dashboard summary for the monitor: current status derived from the latest result per region, time-bucketed chart data, the 24-hour uptime percentage, and the selected window's uptime percentage. - */ - get: operations["getSummary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/resume": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Resume a monitor (set enabled=true) */ - post: operations["resume"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/rotate-token": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Rotate the ping token for a heartbeat monitor - * @description Generates a new ping token. The old token remains valid for 24 hours to allow cron jobs to be updated without downtime. Only supported for HEARTBEAT monitors. - */ - post: operations["rotateToken"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/tags": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all tags applied to a monitor */ - get: operations["getMonitorTags"]; - put?: never; - /** Add tags to a monitor; supports existing tag IDs and inline creation of new tags */ - post: operations["addMonitorTags"]; - /** Remove tags from a monitor by their IDs */ - delete: operations["removeMonitorTags"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Test an existing monitor - * @description Runs the saved config and assertions of an existing monitor once, without persisting any result. Runs synchronously and returns the same shape as the ad-hoc test. - */ - post: operations["testExisting"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/uptime": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get uptime statistics - * @description Returns uptime percentage and latency statistics for the requested time window, computed from continuous aggregates. Uses hourly aggregates for 24h/7d windows and daily aggregates for 30d/90d windows. - */ - get: operations["getUptime"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/versions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List version history for a monitor - * @description Returns a paginated list of mutation snapshots for the monitor, newest first. Each version captures the full monitor config at the time of a PUT /monitors/{id} call. - */ - get: operations["listVersions"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{id}/versions/{version}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a specific version snapshot for a monitor - * @description Returns the full monitor config snapshot captured at the given version number. - */ - get: operations["getVersion"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{monitorId}/alert-channels": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Replace the linked alert channel set for a monitor */ - put: operations["setChannels"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{monitorId}/assertions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Add an assertion to a monitor */ - post: operations["add"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{monitorId}/assertions/{assertionId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update an assertion on a monitor */ - put: operations["update_10"]; - post?: never; - /** Remove an assertion from a monitor */ - delete: operations["remove_1"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{monitorId}/auth": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update authentication config for a monitor */ - put: operations["update_9"]; - /** Set authentication config for a monitor */ - post: operations["set"]; - /** Remove authentication config from a monitor */ - delete: operations["remove"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/{monitorId}/policy": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get incident policy for a monitor - * @description Returns the trigger rules, confirmation settings, and recovery settings for the given monitor. - */ - get: operations["get_5"]; - /** - * Update incident policy for a monitor - * @description Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving. - */ - put: operations["update_8"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/bulk": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Bulk action on monitors - * @description Applies PAUSE, RESUME, DELETE, ADD_TAG, or REMOVE_TAG to a list of monitors. Returns a partial-success response indicating which monitors succeeded and which failed. - */ - post: operations["bulkAction"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitors/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Ad-hoc monitor test - * @description Executes a one-off check from an inline config without saving the monitor. Runs synchronously and returns status code, response time, assertion results, body preview, and headers. - */ - post: operations["testAdHoc"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-dispatches": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all dispatches for an incident - * @description Returns all notification dispatches for the given incident that belong to the authenticated org's policies. Each dispatch includes delivery records for all associated channels. - */ - get: operations["listByIncident"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-dispatches/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a single dispatch with full escalation and delivery history - * @description Returns the dispatch state including current escalation step, acknowledgment info, and all delivery attempts made across every step. - */ - get: operations["getById_3"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-dispatches/{id}/acknowledge": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Acknowledge a notification dispatch - * @description Marks the dispatch as acknowledged. The dispatch must be in DELIVERED or ESCALATING state. Sets acknowledgedAt, acknowledgedBy (actor email), and acknowledgedVia (DASHBOARD). - */ - post: operations["acknowledge"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-policies": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all notification policies for the authenticated org */ - get: operations["list_7"]; - put?: never; - /** Create a notification policy with match rules and escalation chain */ - post: operations["create_8"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-policies/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a notification policy by ID */ - get: operations["getById_1"]; - /** Update a notification policy */ - put: operations["update_7"]; - post?: never; - /** Delete a notification policy */ - delete: operations["delete_6"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-policies/{id}/dispatches": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all dispatches (firing history) for a notification policy */ - get: operations["listDispatches"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notification-policies/{id}/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Dry-run: evaluate a policy's match rules against a supplied incident context */ - post: operations["test_1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notifications": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List notifications for the current user */ - get: operations["list_16"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notifications/{id}/read": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Mark a notification as read */ - put: operations["markRead"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notifications/read-all": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Mark all notifications as read */ - put: operations["markAllRead"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/notifications/unread-count": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get unread notification count */ - get: operations["unreadCount"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/org": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get the current organization */ - get: operations["get_4"]; - /** Update the current organization */ - put: operations["update_6"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/resource-groups": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all resource groups for the authenticated org with health summaries */ - get: operations["list_6"]; - put?: never; - /** Create a new resource group */ - post: operations["create_7"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/resource-groups/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get a resource group by id with member statuses and inherited settings - * @description Pass includeMetrics=true to enrich each member with 24h uptime, chart data, and latency metrics. - */ - get: operations["get_3"]; - /** Update a resource group's name, description, alert policy, inherited settings, and health threshold */ - put: operations["update_5"]; - post?: never; - /** Delete a resource group (cascades to member rows) */ - delete: operations["delete_5"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/resource-groups/{id}/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get the detailed health breakdown for a resource group - * @description Returns member counts, worst-of status, and threshold-based health evaluation. The thresholdStatus field is populated only when a health threshold is configured. - */ - get: operations["getHealth"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/resource-groups/{id}/members": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Add a monitor or service member to a resource group */ - post: operations["addMember_1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/resource-groups/{id}/members/{memberId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove a member from a resource group */ - delete: operations["removeMember_1"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/secrets": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List secrets */ - get: operations["list_5"]; - put?: never; - /** Create secret */ - post: operations["create_6"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/secrets/{key}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update secret */ - put: operations["update_4"]; - post?: never; - /** Delete secret */ - delete: operations["delete_4"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/service-subscriptions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all service subscriptions for the organization */ - get: operations["list_15"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/service-subscriptions/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a subscription by its ID */ - get: operations["get_9"]; - put?: never; - post?: never; - /** - * Remove a subscription by its ID - * @description Removes a specific subscription (whole-service or component-level). No-op if not found. - */ - delete: operations["unsubscribe_1"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/service-subscriptions/{id}/alert-sensitivity": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** - * Update alert sensitivity for a subscription - * @description Controls which external incidents trigger alerts: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents). - */ - patch: operations["updateAlertSensitivity"]; - trace?: never; - }; - "/api/v1/service-subscriptions/{slug}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Subscribe to a service or a component of a service - * @description Idempotent — returns the existing subscription if an identical one exists. Omit the request body or set componentId to null for a whole-service subscription. Free tier: max 10 subscriptions. Paid tier: unlimited. - */ - post: operations["subscribe_1"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all enabled services (cursor-paginated) */ - get: operations["listServices"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a single service by slug or UUID with current status, components, and recent incidents */ - get: operations["getService"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/components": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List active components for a service with current status and inline uptime */ - get: operations["getComponents"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/components/{componentId}/uptime": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get daily uptime data for a component */ - get: operations["getComponentUptime"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/components/uptime": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Batch daily uptime data for all leaf components - * @description Returns daily uptime for every active non-group component with uptime data, keyed by component ID. Replaces N individual per-component uptime calls with a single request. - */ - get: operations["getBatchComponentUptime"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/days/{date}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * One-day rollup for a service: aggregated uptime, per-component impacts, and overlapping incidents - * @description Powers the click/hover-to-expand panel under each uptime bar on the public status page. Single round-trip — components, sums, and overlapping incidents (with affected component names) are returned in one response. - */ - get: operations["getServiceDayDetail"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/incidents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List incident history for a service (paginated) */ - get: operations["listIncidents_1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/incidents/{incidentId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get incident detail with full update timeline */ - get: operations["getIncident_1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/live-status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Lightweight live-status snapshot for polling - * @description Returns only the current overall status, component statuses, and active incident count. Designed for frequent polling with minimal payload. - */ - get: operations["getServiceLiveStatus"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/maintenances": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List scheduled maintenances for a service */ - get: operations["getScheduledMaintenances"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/poll-results": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List poll results for a service (cursor-paginated) */ - get: operations["listPollResults"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/poll-summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get aggregated poll metrics and chart data for a service */ - get: operations["getPollSummary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/{slugOrId}/uptime": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get uptime statistics for a service - * @description Uptime data aggregated across active non-group components. - */ - get: operations["getServiceUptime"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/incidents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List vendor incidents across all services (paginated) - * @description Cross-service vendor incident feed ordered by start date descending. - */ - get: operations["listCrossServiceIncidents"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/services/summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Global status summary across all services - * @description Returns aggregate counts of services by status and a list of services currently experiencing issues. - */ - get: operations["getGlobalStatusSummary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List status pages for the workspace */ - get: operations["list_4"]; - put?: never; - /** Create a status page */ - post: operations["create_5"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a status page */ - get: operations["get_2"]; - /** Update a status page */ - put: operations["update_3"]; - post?: never; - /** Delete a status page */ - delete: operations["delete_3"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/components": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List all components */ - get: operations["listComponents"]; - put?: never; - /** Add a component to the status page */ - post: operations["createComponent"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/components/{componentId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update a component */ - put: operations["updateComponent"]; - post?: never; - /** Remove a component from the status page */ - delete: operations["deleteComponent"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/components/{componentId}/uptime": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get component uptime history (daily rollups) */ - get: operations["componentUptime_1"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/components/reorder": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Batch reorder components (and optionally move between groups) */ - put: operations["reorderComponents"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/domains": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List custom domains */ - get: operations["listDomains"]; - put?: never; - /** Add a custom domain */ - post: operations["addDomain"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/domains/{domainId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove a custom domain */ - delete: operations["removeDomain"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/domains/{domainId}/verify": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Trigger domain verification check */ - post: operations["verifyDomain"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/groups": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List component groups with nested components */ - get: operations["listGroups"]; - put?: never; - /** Create a component group */ - post: operations["createGroup"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/groups/{groupId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Update a component group */ - put: operations["updateGroup"]; - post?: never; - /** Delete a component group (components become ungrouped) */ - delete: operations["deleteGroup"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/incidents": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List incidents for this status page (filterable by status, paginated) */ - get: operations["listIncidents"]; - put?: never; - /** Create a status page incident (manual) */ - post: operations["createIncident"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/incidents/{incidentId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get incident details with timeline */ - get: operations["getIncident"]; - /** Update an incident */ - put: operations["updateIncident"]; - post?: never; - /** Delete an incident */ - delete: operations["deleteIncident"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/incidents/{incidentId}/dismiss": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Dismiss a draft incident (deletes it permanently) */ - post: operations["dismissIncident"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/incidents/{incidentId}/publish": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Publish a draft incident (sets publishedAt, applies component statuses, notifies subscribers) */ - post: operations["publishIncident"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/incidents/{incidentId}/updates": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Post an incident timeline update */ - post: operations["postIncidentUpdate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/layout/reorder": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - /** Reorder page-level layout: groups and ungrouped components share one ordering */ - put: operations["reorderLayout"]; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/subscribers": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List confirmed subscribers (paginated) */ - get: operations["listSubscribers"]; - put?: never; - /** Add a subscriber (immediately confirmed, skips double opt-in) */ - post: operations["addSubscriber"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/status-pages/{id}/subscribers/{subscriberId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove a subscriber */ - delete: operations["removeSubscriber"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/tags": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List tags for the authenticated organization */ - get: operations["list_3"]; - put?: never; - /** Create a new tag */ - post: operations["create_4"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/tags/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a tag by ID */ - get: operations["getById"]; - /** Update a tag's name and/or color */ - put: operations["update_2"]; - post?: never; - /** Delete a tag (cascades to all monitor associations) */ - delete: operations["delete_2"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/vaults/rotate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Rotate DEK - * @description Generates a new Data Encryption Key, re-encrypts all secrets and alert-channel configs, and bumps the vault version. Admin-only. Pipeline DEK caches expire within ~10 minutes. - */ - post: operations["rotateDek"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List webhook endpoints for the authenticated org */ - get: operations["list_2"]; - put?: never; - /** Register a new webhook endpoint */ - post: operations["create_3"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get a single webhook endpoint */ - get: operations["get_1"]; - /** Update a webhook endpoint */ - put: operations["update_1"]; - post?: never; - /** Delete a webhook endpoint */ - delete: operations["delete_1"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/{id}/deliveries": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List recent deliveries for a webhook endpoint */ - get: operations["listDeliveries"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/{id}/test": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Send a test delivery to a webhook endpoint */ - post: operations["test"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * List all available webhook event types - * @description Returns the full catalog of supported outbound webhook event types with their surface grouping and human-readable descriptions. Use this to populate subscription checkboxes when creating or updating a webhook endpoint. - */ - get: operations["listEvents"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/signing-secret": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get signing secret metadata for the authenticated org */ - get: operations["getSigningSecretInfo"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/webhooks/signing-secret/rotate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Generate or rotate the organization webhook signing secret */ - post: operations["rotateSigningSecret"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/workspaces": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** List workspaces */ - get: operations["list_1"]; - put?: never; - /** Create workspace */ - post: operations["create_2"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/workspaces/{workspaceId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get workspace by ID */ - get: operations["get"]; - /** Update workspace */ - put: operations["update"]; - post?: never; - /** Delete workspace */ - delete: operations["delete"]; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - /** @description Request to acquire a deploy lock for the current workspace */ - AcquireDeployLockRequest: { - /** @description Identity of the lock requester (e.g. hostname, CI job ID) */ - lockedBy: string; - /** - * Format: int32 - * @description Lock TTL in minutes (default: 30, max: 60) - * @example 30 - */ - ttlMinutes?: number | null; - }; - AddCustomDomainRequest: { - /** @description Custom hostname, e.g. status.acme.com */ - hostname: string; - }; - AddIncidentUpdateRequest: { - /** @description Update message or post-mortem notes */ - body?: string | null; - /** - * @description Updated incident status; null to keep current status - * @enum {string|null} - */ - newStatus?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED" | null; - /** @description Whether to notify subscribers of this update */ - notifySubscribers: boolean; - }; - /** @description Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both. */ - AddMonitorTagsRequest: { - /** @description IDs of existing org tags to attach */ - tagIds?: string[] | null; - /** @description New tags to create (if not already present) and attach */ - newTags?: components["schemas"]["NewTagRequest"][] | null; - }; - /** @description Request body for adding a member to a resource group */ - AddResourceGroupMemberRequest: { - /** @description Type of member: 'monitor' or 'service' */ - memberType: string; - /** - * Format: uuid - * @description ID of the monitor or service to add - */ - memberId: string; - }; - AdminAddSubscriberRequest: { - /** - * Format: email - * @description Email address to add as a confirmed subscriber - */ - email: string; - }; - /** @description Updated affected components; null preserves current */ - AffectedComponent: { - /** - * Format: uuid - * @description Status page component ID - */ - componentId: string; - /** - * @description Component status during this incident - * @enum {string} - */ - status: "OPERATIONAL" | "DEGRADED_PERFORMANCE" | "PARTIAL_OUTAGE" | "MAJOR_OUTAGE" | "UNDER_MAINTENANCE"; - }; - /** @description Non-sensitive alert channel configuration metadata */ - AlertChannelDisplayConfig: { - /** @description Email recipients list (email channels) */ - recipients?: string[] | null; - /** @description OpsGenie API region: us or eu */ - region?: string | null; - /** @description PagerDuty severity override (critical, error, warning, info) */ - severityOverride?: string | null; - /** @description Discord role ID to mention in notifications */ - mentionRoleId?: string | null; - /** @description Custom HTTP headers for webhook requests */ - customHeaders?: { - [key: string]: string | null; - } | null; - }; - /** @description Alert channel with non-sensitive configuration metadata */ - AlertChannelDto: { - /** - * Format: uuid - * @description Unique alert channel identifier - */ - id: string; - /** @description Human-readable channel name */ - name: string; - /** - * @description Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL) - * @enum {string} - */ - channelType: "email" | "webhook" | "slack" | "pagerduty" | "opsgenie" | "teams" | "discord"; - displayConfig?: components["schemas"]["AlertChannelDisplayConfig"] | null; - /** - * Format: date-time - * @description Timestamp when the channel was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the channel was last updated - */ - updatedAt: string; - /** @description SHA-256 hash of the channel config; use for change detection */ - configHash?: string | null; - /** - * Format: date-time - * @description Timestamp of the most recent delivery attempt - */ - lastDeliveryAt?: string | null; - /** @description Outcome of the most recent delivery (SUCCESS, FAILED, etc.) */ - lastDeliveryStatus?: string | null; - }; - /** @description Delivery record for a single channel within a notification dispatch */ - AlertDeliveryDto: { - /** Format: uuid */ - id: string; - /** - * Format: uuid - * @description Incident that triggered this delivery - */ - incidentId: string; - /** - * Format: uuid - * @description Notification dispatch that created this delivery - */ - dispatchId?: string | null; - /** - * Format: uuid - * @description Alert channel ID - */ - channelId: string; - /** @description Human-readable channel name */ - channel: string; - /** @description Alert channel type (e.g. slack, email, webhook) */ - channelType: string; - /** - * @description Current delivery status - * @enum {string} - */ - status: "PENDING" | "DELIVERED" | "RETRY_PENDING" | "FAILED" | "CANCELLED"; - /** - * @description Incident lifecycle event that triggered this delivery - * @enum {string} - */ - eventType: "INCIDENT_CREATED" | "INCIDENT_RESOLVED" | "INCIDENT_REOPENED"; - /** - * Format: int32 - * @description 1-based escalation step this delivery belongs to - */ - stepNumber?: number; - /** - * Format: int32 - * @description Fire sequence within the step: 1 = initial, 2+ = repeat re-fires - */ - fireCount?: number; - /** - * Format: int32 - * @description Number of delivery attempts made - */ - attemptCount?: number; - /** - * Format: date-time - * @description When the last attempt was made - */ - lastAttemptAt?: string | null; - /** - * Format: date-time - * @description When the next retry is scheduled (null if not retrying) - */ - nextRetryAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the delivery was confirmed (null if not yet delivered) - */ - deliveredAt?: string | null; - /** @description Error message from the last failed attempt */ - errorMessage?: string | null; - /** Format: date-time */ - createdAt: string; - }; - ApiKeyAuthConfig: Omit & { - /** @description HTTP header name that carries the API key */ - headerName: string; - /** - * Format: uuid - * @description Vault secret ID for the API key value - */ - vaultSecretId?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "api_key"; - }; - /** @description Created API key with the full key value — store it now, it won't be shown again */ - ApiKeyCreateResponse: { - /** - * Format: int32 - * @description Unique API key identifier - */ - id?: number; - /** @description Human-readable name for this API key */ - name: string; - /** @description Full API key value in dh_live_* format; store this now */ - key: string; - /** - * Format: date-time - * @description Timestamp when the key was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the key expires; null if no expiration - */ - expiresAt?: string | null; - }; - /** @description API key for programmatic access to the DevHelm API */ - ApiKeyDto: { - /** - * Format: int32 - * @description Unique API key identifier - */ - id?: number; - /** @description Human-readable name for this API key */ - name: string; - /** @description Full API key value in dh_live_* format */ - key: string; - /** - * Format: date-time - * @description Timestamp when the key was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the key was last updated - */ - updatedAt: string; - /** - * Format: date-time - * @description Timestamp of the most recent API call; null if never used - */ - lastUsedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the key was revoked; null if active - */ - revokedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the key expires; null if no expiration - */ - expiresAt?: string | null; - }; - /** @description New assertion configuration (full replacement) */ - AssertionConfig: { - type: string; - }; - /** @description Result of evaluating a single assertion against a check result */ - AssertionResultDto: { - /** - * @description Assertion type - * @example status_code - */ - type: string; - /** @description Whether the assertion passed */ - passed?: boolean; - /** - * @description Assertion severity - * @enum {string} - */ - severity: "fail" | "warn"; - /** @description Human-readable result message */ - message?: string | null; - /** - * @description Expected value - * @example 200 - */ - expected?: string | null; - /** - * @description Actual value observed - * @example 503 - */ - actual?: string | null; - }; - AssertionTestResultDto: { - /** - * @description Assertion type evaluated - * @enum {string} - */ - assertionType: "status_code" | "response_time" | "body_contains" | "json_path" | "header_value" | "regex_body" | "dns_resolves" | "dns_response_time" | "dns_expected_ips" | "dns_expected_cname" | "dns_record_contains" | "dns_record_equals" | "dns_txt_contains" | "dns_min_answers" | "dns_max_answers" | "dns_response_time_warn" | "dns_ttl_low" | "dns_ttl_high" | "mcp_connects" | "mcp_response_time" | "mcp_has_capability" | "mcp_tool_available" | "mcp_min_tools" | "mcp_protocol_version" | "mcp_response_time_warn" | "mcp_tool_count_changed" | "ssl_expiry" | "response_size" | "redirect_count" | "redirect_target" | "response_time_warn" | "tcp_connects" | "tcp_response_time" | "tcp_response_time_warn" | "icmp_reachable" | "icmp_response_time" | "icmp_response_time_warn" | "icmp_packet_loss" | "heartbeat_received" | "heartbeat_max_interval" | "heartbeat_interval_drift" | "heartbeat_payload_contains"; - /** @description Whether the assertion passed */ - passed?: boolean; - /** - * @description Assertion severity: FAIL or WARN - * @enum {string} - */ - severity: "fail" | "warn"; - /** @description Human-readable result description */ - message: string; - /** @description Expected value */ - expected?: string | null; - /** @description Actual value observed during the test */ - actual?: string | null; - }; - AuditEventDto: { - /** - * Format: int64 - * @description Unique audit event identifier - */ - id?: number; - /** - * Format: int32 - * @description User ID who performed the action; null for system actions - */ - actorId?: number | null; - /** @description Email of the actor; null for system actions */ - actorEmail?: string | null; - /** @description Audit action type (e.g. monitor.created, api_key.revoked) */ - action: string; - /** @description Type of resource affected (e.g. monitor, api_key) */ - resourceType?: string | null; - /** @description ID of the affected resource */ - resourceId?: string | null; - /** @description Human-readable name of the affected resource */ - resourceName?: string | null; - /** @description Additional context about the action */ - metadata?: { - [key: string]: Record | null; - } | null; - /** - * Format: date-time - * @description Timestamp when the action was performed - */ - createdAt: string; - }; - /** @description Identity, organization, plan, and rate-limit info for the authenticated API key */ - AuthMeResponse: { - key: components["schemas"]["KeyInfo"]; - organization: components["schemas"]["OrgInfo"]; - plan: components["schemas"]["PlanInfo"]; - rateLimits: components["schemas"]["RateLimitInfo"]; - }; - BasicAuthConfig: Omit & { - /** - * Format: uuid - * @description Vault secret ID holding Basic auth username and password - */ - vaultSecretId?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "basic"; - }; - BearerAuthConfig: Omit & { - /** - * Format: uuid - * @description Vault secret ID holding the bearer token value - */ - vaultSecretId?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "bearer"; - }; - BodyContainsAssertion: Omit & { - /** @description Substring that must appear in the response body */ - substring: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "body_contains"; - }; - /** @description Request body for performing a bulk action on multiple monitors */ - BulkMonitorActionRequest: { - /** @description IDs of monitors to act on (max 200) */ - monitorIds: string[]; - /** - * @description Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG - * @enum {string} - */ - action: "PAUSE" | "RESUME" | "DELETE" | "ADD_TAG" | "REMOVE_TAG"; - /** @description Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG) */ - tagIds?: string[] | null; - /** @description New tags to create and attach (only for ADD_TAG) */ - newTags?: components["schemas"]["NewTagRequest"][] | null; - }; - /** @description Result of a bulk monitor action, including partial-success details */ - BulkMonitorActionResult: { - /** @description IDs of monitors on which the action succeeded */ - succeeded: string[]; - /** @description Monitors on which the action failed, with the reason for each failure */ - failed: components["schemas"]["FailureDetail"][]; - }; - /** @description Service category with its count of catalog entries */ - CategoryDto: { - /** @description Category name (e.g. CI/CD, Cloud, Payments) */ - category: string; - /** - * Format: int64 - * @description Number of services in this category - */ - serviceCount?: number; - }; - /** @description Update an organization member's role */ - ChangeRoleRequest: { - /** - * @description New role to assign - * @enum {string} - */ - orgRole: "OWNER" | "ADMIN" | "MEMBER"; - }; - /** @description Update an organization member's status */ - ChangeStatusRequest: { - /** - * @description New membership status (ACTIVE or SUSPENDED) - * @enum {string} - */ - status: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; - }; - /** @description New channel configuration (full replacement, not partial update) */ - ChannelConfig: { - channelType: string; - }; - /** @description Aggregated metrics for a time bucket */ - ChartBucketDto: { - /** - * Format: date-time - * @description Start of the time bucket (ISO 8601) - * @example 2026-03-12T10:00:00Z - */ - bucket: string; - /** - * Format: double - * @description Uptime percentage for this bucket; null when no data - * @example 100 - */ - uptimePercent?: number | null; - /** - * Format: double - * @description Weighted average latency in milliseconds for this bucket - * @example 120.3 - */ - avgLatencyMs?: number | null; - /** - * Format: double - * @description 95th percentile latency in milliseconds (max across regions) - * @example 250 - */ - p95LatencyMs?: number | null; - /** - * Format: double - * @description 99th percentile latency in milliseconds (max across regions) - * @example 480 - */ - p99LatencyMs?: number | null; - }; - /** @description Type-specific details captured during a check execution */ - CheckResultDetailsDto: { - /** - * Format: int32 - * @description HTTP status code of the response - * @example 200 - */ - statusCode?: number | null; - /** @description HTTP response headers */ - responseHeaders?: { - [key: string]: (string | null)[] | null; - } | null; - /** @description Raw response body snapshot (may be HTML, XML, JSON, or plain text) */ - responseBodySnapshot?: string | null; - /** @description Individual assertion evaluation results */ - assertionResults?: components["schemas"]["AssertionResultDto"][] | null; - tlsInfo?: components["schemas"]["TlsInfoDto"] | null; - /** - * Format: int32 - * @description Number of HTTP redirects followed - * @example 2 - */ - redirectCount?: number | null; - /** @description Final URL after redirects */ - redirectTarget?: string | null; - /** - * Format: int32 - * @description Response body size in bytes - * @example 4096 - */ - responseSizeBytes?: number | null; - checkDetails?: Omit | null; - }; - /** @description A single check result from a monitor run */ - CheckResultDto: { - /** - * Format: uuid - * @description Unique identifier of the check result - */ - id: string; - /** - * Format: date-time - * @description Timestamp when the check was executed (ISO 8601) - */ - timestamp: string; - /** - * @description Region where the check was executed - * @example us-east - */ - region: string; - /** - * Format: int32 - * @description Response time in milliseconds - * @example 123 - */ - responseTimeMs?: number | null; - /** - * @description Whether the check passed - * @example true - */ - passed?: boolean; - /** @description Reason for failure when passed=false */ - failureReason?: string | null; - /** @description Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing */ - severityHint?: string | null; - details?: components["schemas"]["CheckResultDetailsDto"] | null; - /** - * Format: uuid - * @description Unique execution trace ID for cross-service correlation - */ - checkId?: string | null; - }; - /** @description Check-type-specific details — polymorphic by check_type discriminator */ - CheckTypeDetailsDto: { - check_type: string; - }; - /** @description One component's uptime contribution for the day */ - ComponentImpact: { - /** - * Format: uuid - * @description Status page component UUID - */ - componentId: string; - /** @description Component display name */ - componentName: string; - /** @description Parent group display name when the component belongs to a group */ - groupName?: string | null; - /** - * Format: double - * @description Computed uptime % for this component on this day - */ - uptimePercentage?: number; - /** - * Format: int32 - * @description Seconds of partial outage observed on this day - */ - partialOutageSeconds?: number; - /** - * Format: int32 - * @description Seconds of major outage observed on this day - */ - majorOutageSeconds?: number; - }; - /** @description A single component position */ - ComponentPosition: { - /** - * Format: uuid - * @description Component ID - */ - componentId: string; - /** - * Format: int32 - * @description New display order (0-based) - */ - displayOrder?: number; - /** - * Format: uuid - * @description Target group ID, null for ungrouped - */ - groupId?: string | null; - }; - /** @description Current status of each active component */ - ComponentStatusDto: { - /** @description Component UUID */ - id: string; - /** @description Human-readable component name */ - name: string; - /** @description Current component status, e.g. operational, degraded_performance */ - status: string; - }; - /** @description Daily uptime data for a status page component */ - ComponentUptimeDayDto: { - /** - * Format: date-time - * @description Start-of-day timestamp for this bucket (UTC midnight) - */ - date: string; - /** - * Format: int32 - * @description Seconds of partial outage on this day - */ - partialOutageSeconds?: number; - /** - * Format: int32 - * @description Seconds of major outage on this day - */ - majorOutageSeconds?: number; - /** - * Format: double - * @description Computed uptime percentage using weighted formula - */ - uptimePercentage?: number; - /** @description Incidents that overlapped this day */ - incidents?: components["schemas"]["IncidentRef"][] | null; - }; - /** @description Inline uptime percentages for 24h, 7d, 30d */ - ComponentUptimeSummaryDto: { - /** - * Format: double - * @description Uptime percentage over the last 24 hours - * @example 99.95 - */ - day?: number | null; - /** - * Format: double - * @description Uptime percentage over the last 7 days - * @example 99.98 - */ - week?: number | null; - /** - * Format: double - * @description Uptime percentage over the last 30 days - * @example 99.92 - */ - month?: number | null; - /** - * @description Data source: vendor_reported or incident_derived - * @example vendor_reported - */ - source: string; - }; - /** @description Multi-region confirmation settings */ - ConfirmationPolicy: { - /** - * @description How incident confirmation is coordinated across regions - * @enum {string} - */ - type: "multi_region"; - /** - * Format: int32 - * @description Minimum failing regions required to confirm an incident - */ - minRegionsFailing?: number; - /** - * Format: int32 - * @description Maximum seconds to wait for enough regions to fail after first trigger - */ - maxWaitSeconds?: number; - }; - CreateAlertChannelRequest: { - /** @description Human-readable name for this alert channel */ - name: string; - config: components["schemas"]["DiscordChannelConfig"] | components["schemas"]["EmailChannelConfig"] | components["schemas"]["OpsGenieChannelConfig"] | components["schemas"]["PagerDutyChannelConfig"] | components["schemas"]["SlackChannelConfig"] | components["schemas"]["TeamsChannelConfig"] | components["schemas"]["WebhookChannelConfig"]; - }; - CreateApiKeyRequest: { - /** @description Human-readable name to identify this API key */ - name: string; - /** - * Format: date-time - * @description Optional expiration timestamp in ISO 8601 format - */ - expiresAt?: string | null; - }; - /** @description Replace all assertions; null preserves current */ - CreateAssertionRequest: { - config: components["schemas"]["BodyContainsAssertion"] | components["schemas"]["DnsExpectedCnameAssertion"] | components["schemas"]["DnsExpectedIpsAssertion"] | components["schemas"]["DnsMaxAnswersAssertion"] | components["schemas"]["DnsMinAnswersAssertion"] | components["schemas"]["DnsRecordContainsAssertion"] | components["schemas"]["DnsRecordEqualsAssertion"] | components["schemas"]["DnsResolvesAssertion"] | components["schemas"]["DnsResponseTimeAssertion"] | components["schemas"]["DnsResponseTimeWarnAssertion"] | components["schemas"]["DnsTtlHighAssertion"] | components["schemas"]["DnsTtlLowAssertion"] | components["schemas"]["DnsTxtContainsAssertion"] | components["schemas"]["HeaderValueAssertion"] | components["schemas"]["HeartbeatIntervalDriftAssertion"] | components["schemas"]["HeartbeatMaxIntervalAssertion"] | components["schemas"]["HeartbeatPayloadContainsAssertion"] | components["schemas"]["HeartbeatReceivedAssertion"] | components["schemas"]["IcmpPacketLossAssertion"] | components["schemas"]["IcmpReachableAssertion"] | components["schemas"]["IcmpResponseTimeAssertion"] | components["schemas"]["IcmpResponseTimeWarnAssertion"] | components["schemas"]["JsonPathAssertion"] | components["schemas"]["McpConnectsAssertion"] | components["schemas"]["McpHasCapabilityAssertion"] | components["schemas"]["McpMinToolsAssertion"] | components["schemas"]["McpProtocolVersionAssertion"] | components["schemas"]["McpResponseTimeAssertion"] | components["schemas"]["McpResponseTimeWarnAssertion"] | components["schemas"]["McpToolAvailableAssertion"] | components["schemas"]["McpToolCountChangedAssertion"] | components["schemas"]["RedirectCountAssertion"] | components["schemas"]["RedirectTargetAssertion"] | components["schemas"]["RegexBodyAssertion"] | components["schemas"]["ResponseSizeAssertion"] | components["schemas"]["ResponseTimeAssertion"] | components["schemas"]["ResponseTimeWarnAssertion"] | components["schemas"]["SslExpiryAssertion"] | components["schemas"]["StatusCodeAssertion"] | components["schemas"]["TcpConnectsAssertion"] | components["schemas"]["TcpResponseTimeAssertion"] | components["schemas"]["TcpResponseTimeWarnAssertion"]; - /** - * @description Outcome severity: FAIL (fails the check) or WARN (warns without failing) - * @enum {string} - */ - severity: "fail" | "warn"; - }; - CreateEnvironmentRequest: { - /** @description Human-readable environment name */ - name: string; - /** @description URL-safe identifier (lowercase alphanumeric, hyphens, underscores) */ - slug: string; - /** @description Initial key-value variable pairs for this environment */ - variables?: { - [key: string]: string | null; - } | null; - /** @description Whether this is the default environment for new monitors */ - isDefault?: boolean; - }; - /** @description Invite a new member to the organization by email */ - CreateInviteRequest: { - /** - * Format: email - * @description Email address to invite - */ - email: string; - /** - * @description Role to assign on acceptance - * @enum {string} - */ - roleOffered: "OWNER" | "ADMIN" | "MEMBER"; - }; - CreateMaintenanceWindowRequest: { - /** - * Format: uuid - * @description Monitor to attach this maintenance window to; null for org-wide - */ - monitorId?: string | null; - /** - * Format: date-time - * @description Scheduled start of the maintenance window (ISO 8601) - */ - startsAt: string; - /** - * Format: date-time - * @description Scheduled end of the maintenance window (ISO 8601) - */ - endsAt: string; - /** @description iCal RRULE for recurring windows (max 100 chars); null for one-time */ - repeatRule?: string | null; - /** @description Human-readable reason for the maintenance */ - reason?: string | null; - /** @description Whether to suppress alerts during this window (default: true) */ - suppressAlerts?: boolean | null; - }; - CreateManualIncidentRequest: { - /** @description Short summary of the incident */ - title: string; - /** - * @description Incident severity: DOWN, DEGRADED, or MAINTENANCE - * @enum {string} - */ - severity: "DOWN" | "DEGRADED" | "MAINTENANCE"; - /** - * Format: uuid - * @description Monitor to associate with this incident - */ - monitorId?: string | null; - /** @description Detailed description or context for the incident */ - body?: string | null; - }; - CreateMonitorRequest: { - /** @description Human-readable name for this monitor */ - name: string; - /** - * @description Monitor protocol type - * @enum {string} - */ - type: "HTTP" | "DNS" | "MCP_SERVER" | "TCP" | "ICMP" | "HEARTBEAT"; - config: components["schemas"]["DnsMonitorConfig"] | components["schemas"]["HeartbeatMonitorConfig"] | components["schemas"]["HttpMonitorConfig"] | components["schemas"]["IcmpMonitorConfig"] | components["schemas"]["McpServerMonitorConfig"] | components["schemas"]["TcpMonitorConfig"]; - /** - * Format: int32 - * @description Check frequency in seconds (30–86400); null defaults to plan minimum (60s on most paid plans) - */ - frequencySeconds?: number | null; - /** @description Whether the monitor is active (default: true) */ - enabled?: boolean | null; - /** @description Probe regions to run checks from, e.g. us-east, eu-west */ - regions?: string[] | null; - /** - * @description Who manages this monitor: DASHBOARD or CLI - * @enum {string} - */ - managedBy: "DASHBOARD" | "CLI" | "TERRAFORM"; - /** - * Format: uuid - * @description Environment to associate with this monitor - */ - environmentId?: string | null; - /** @description Assertions to evaluate against each check result */ - assertions?: components["schemas"]["CreateAssertionRequest"][] | null; - auth?: Omit | null; - incidentPolicy?: components["schemas"]["UpdateIncidentPolicyRequest"] | null; - /** @description Alert channels to notify when this monitor triggers */ - alertChannelIds?: string[] | null; - tags?: components["schemas"]["AddMonitorTagsRequest"] | null; - }; - /** @description Request body for creating a notification policy */ - CreateNotificationPolicyRequest: { - /** @description Human-readable name for this policy */ - name: string; - /** @description Match rules to evaluate (all must pass; omit or empty for catch-all) */ - matchRules: components["schemas"]["MatchRule"][]; - escalation: components["schemas"]["EscalationChain"]; - /** - * @description Whether this policy is enabled (default true) - * @default true - */ - enabled: boolean; - /** - * Format: int32 - * @description Evaluation priority; higher value = evaluated first (default 0) - * @default 0 - */ - priority: number; - }; - /** @description Request body for creating a resource group */ - CreateResourceGroupRequest: { - /** @description Human-readable name for this group */ - name: string; - /** @description Optional description */ - description?: string | null; - /** - * Format: uuid - * @description Optional notification policy to apply for this group - */ - alertPolicyId?: string | null; - /** - * Format: int32 - * @description Default check frequency in seconds applied to members (30–86400) - */ - defaultFrequency?: number | null; - /** @description Default regions applied to member monitors */ - defaultRegions?: string[] | null; - defaultRetryStrategy?: components["schemas"]["RetryStrategy"] | null; - /** @description Default alert channel IDs applied to member monitors */ - defaultAlertChannels?: string[] | null; - /** - * Format: uuid - * @description Default environment ID applied to member monitors - */ - defaultEnvironmentId?: string | null; - /** - * @description Health threshold type: COUNT or PERCENTAGE - * @enum {string|null} - */ - healthThresholdType?: "COUNT" | "PERCENTAGE" | null; - /** @description Health threshold value: count (0+) or percentage (0–100) */ - healthThresholdValue?: number | null; - /** @description Suppress member-level alert notifications when group manages alerting */ - suppressMemberAlerts?: boolean | null; - /** - * Format: int32 - * @description Confirmation delay in seconds before group incident creation (0–600) - */ - confirmationDelaySeconds?: number | null; - /** - * Format: int32 - * @description Recovery cooldown in minutes after group incident resolves (0–60) - */ - recoveryCooldownMinutes?: number | null; - }; - CreateSecretRequest: { - /** @description Unique secret key within the workspace (max 255 chars) */ - key: string; - /** @description Secret value, stored encrypted (max 32KB) */ - value: string; - }; - CreateStatusPageComponentGroupRequest: { - /** @description Group display name */ - name: string; - /** @description Optional group description */ - description?: string | null; - /** - * Format: int32 - * @description Position in the group list - */ - displayOrder?: number | null; - /** @description Whether the group is collapsed by default (default: true) */ - collapsed?: boolean | null; - }; - CreateStatusPageComponentRequest: { - /** @description Component display name */ - name: string; - /** @description Optional description shown on expand */ - description?: string | null; - /** - * @description Component type: MONITOR, GROUP, or STATIC - * @enum {string} - */ - type: "MONITOR" | "GROUP" | "STATIC"; - /** - * Format: uuid - * @description Monitor ID (required when type=MONITOR) - */ - monitorId?: string | null; - /** - * Format: uuid - * @description Resource group ID (required when type=GROUP) - */ - resourceGroupId?: string | null; - /** - * Format: uuid - * @description Component group ID for visual grouping - */ - groupId?: string | null; - /** @description Whether to show the uptime bar (default: true) */ - showUptime?: boolean | null; - /** - * Format: int32 - * @description Position in the component list - */ - displayOrder?: number | null; - /** @description Exclude from overall status calculation (default: false, use true for third-party deps) */ - excludeFromOverall?: boolean | null; - /** - * Format: date - * @description Date from which to start showing uptime data - */ - startDate?: string | null; - }; - CreateStatusPageIncidentRequest: { - /** @description Customer-facing incident title */ - title: string; - /** - * @description Initial status (default: INVESTIGATING) - * @enum {string|null} - */ - status?: "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED" | null; - /** - * @description Impact level: NONE, MINOR, MAJOR, or CRITICAL - * @enum {string} - */ - impact: "NONE" | "MINOR" | "MAJOR" | "CRITICAL"; - /** @description Initial update body in markdown */ - body: string; - /** @description Component IDs affected by this incident */ - affectedComponents?: components["schemas"]["AffectedComponent"][] | null; - /** @description Whether this is a scheduled maintenance (default: false) */ - scheduled?: boolean | null; - /** - * Format: date-time - * @description Maintenance start time (required when scheduled=true) - */ - scheduledFor?: string | null; - /** - * Format: date-time - * @description Maintenance end time - */ - scheduledUntil?: string | null; - /** @description Auto-resolve at scheduledUntil (default: false) */ - autoResolve?: boolean | null; - /** @description Whether to email confirmed subscribers about this incident (default: true) */ - notifySubscribers?: boolean | null; - }; - CreateStatusPageIncidentUpdateRequest: { - /** - * @description Incident status at this point in the timeline - * @enum {string} - */ - status: "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED"; - /** @description Update body in markdown */ - body: string; - /** @description Whether to email confirmed subscribers about this update (default: true) */ - notifySubscribers?: boolean | null; - /** @description Updated affected components; null preserves current */ - affectedComponents?: components["schemas"]["AffectedComponent"][] | null; - }; - CreateStatusPageRequest: { - /** @description Human-readable name for this status page */ - name: string; - /** @description URL slug (lowercase, hyphens, globally unique) */ - slug: string; - /** @description Optional description shown below the page header */ - description?: string | null; - branding?: components["schemas"]["StatusPageBranding"] | null; - /** - * @description Page visibility: PUBLIC, PASSWORD, or IP_RESTRICTED (default: PUBLIC) - * @enum {string|null} - */ - visibility?: "PUBLIC" | "PASSWORD" | "IP_RESTRICTED" | null; - /** @description Whether the page is enabled (default: true) */ - enabled?: boolean | null; - /** - * @description Incident mode: MANUAL, REVIEW, or AUTOMATIC (default: AUTOMATIC) - * @enum {string|null} - */ - incidentMode?: "MANUAL" | "REVIEW" | "AUTOMATIC" | null; - }; - /** @description Request body for creating a tag */ - CreateTagRequest: { - /** @description Tag name, unique within the org */ - name: string; - /** @description Hex color code (defaults to #6B7280 if omitted) */ - color?: string | null; - }; - CreateWebhookEndpointRequest: { - /** @description HTTPS endpoint that receives webhook event payloads */ - url: string; - /** @description Optional human-readable description */ - description?: string | null; - /** @description Event types to deliver, e.g. monitor.created, incident.resolved */ - subscribedEvents: string[]; - }; - /** @description Create a new workspace within the organization */ - CreateWorkspaceRequest: { - /** @description Workspace name */ - name: string; - }; - /** @description Cursor-paginated response for time-series and append-only data */ - CursorPage: { - /** @description Items on this page */ - data: Record[]; - /** @description Opaque cursor for the next page; null when there are no more results */ - nextCursor?: string | null; - /** @description Whether more results exist beyond this page */ - hasMore?: boolean; - }; - /** @description Cursor-paginated response for time-series and append-only data */ - CursorPageCheckResultDto: { - /** @description Items on this page */ - data: components["schemas"]["CheckResultDto"][]; - /** @description Opaque cursor for the next page; null when there are no more results */ - nextCursor?: string | null; - /** @description Whether more results exist beyond this page */ - hasMore?: boolean; - }; - /** @description Cursor-paginated response for time-series and append-only data */ - CursorPageServiceCatalogDto: { - /** @description Items on this page */ - data: components["schemas"]["ServiceCatalogDto"][]; - /** @description Opaque cursor for the next page; null when there are no more results */ - nextCursor?: string | null; - /** @description Whether more results exist beyond this page */ - hasMore?: boolean; - }; - /** @description Cursor-paginated response for time-series and append-only data */ - CursorPageServicePollResultDto: { - /** @description Items on this page */ - data: components["schemas"]["ServicePollResultDto"][]; - /** @description Opaque cursor for the next page; null when there are no more results */ - nextCursor?: string | null; - /** @description Whether more results exist beyond this page */ - hasMore?: boolean; - }; - /** @description Combined dashboard overview for monitors and incidents */ - DashboardOverviewDto: { - monitors: components["schemas"]["MonitorsSummaryDto"]; - incidents: components["schemas"]["IncidentsSummaryDto"]; - }; - /** @description Incident that overlapped the day */ - DayIncident: { - /** - * Format: uuid - * @description Status page incident UUID - */ - id: string; - /** @description Incident title */ - title: string; - /** - * @description Lifecycle status (investigating, identified, monitoring, resolved, …) - * @enum {string} - */ - status: "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED"; - /** - * @description Severity bucket (none, minor, major, critical) - * @enum {string} - */ - impact: "NONE" | "MINOR" | "MAJOR" | "CRITICAL"; - /** @description True for scheduled maintenances; false for unplanned incidents */ - scheduled?: boolean; - /** - * Format: date-time - * @description Incident start timestamp - */ - startedAt?: string | null; - /** - * Format: date-time - * @description Incident resolved timestamp; null while still active - */ - resolvedAt?: string | null; - /** @description Display names of components affected by this incident (deduplicated) */ - affectedComponentNames: string[]; - }; - /** @description Result of a data encryption key rotation operation */ - DekRotationResultDto: { - /** - * Format: int32 - * @description DEK version before rotation - */ - previousDekVersion?: number; - /** - * Format: int32 - * @description DEK version after rotation - */ - newDekVersion?: number; - /** - * Format: int32 - * @description Number of secrets re-encrypted with the new DEK - */ - secretsReEncrypted?: number; - /** - * Format: int32 - * @description Number of alert channels re-encrypted with the new DEK - */ - channelsReEncrypted?: number; - /** - * Format: date-time - * @description Timestamp when the rotation was performed - */ - rotatedAt: string; - }; - /** @description Summary of policies affected by channel deletion */ - DeleteChannelResult: { - /** - * Format: int32 - * @description Number of notification policies whose escalation steps were modified - */ - affectedPolicies: number; - /** - * Format: int32 - * @description Number of notification policies disabled because they had no remaining channels - */ - disabledPolicies: number; - }; - /** @description Single delivery attempt with request/response audit data */ - DeliveryAttemptDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - deliveryId: string; - /** - * Format: int32 - * @description 1-based attempt number - */ - attemptNumber?: number; - /** @description Outcome: SUCCESS, FAILED, TIMEOUT, ERROR */ - status: string; - /** - * Format: int32 - * @description HTTP response status code from the external service - */ - responseStatusCode?: number | null; - /** @description JSON payload sent to the external service */ - requestPayload?: string | null; - /** @description Response body from the external service (truncated) */ - responseBody?: string | null; - /** @description Error message if the attempt failed */ - errorMessage?: string | null; - /** - * Format: int32 - * @description Round-trip time in milliseconds - */ - responseTimeMs?: number | null; - /** @description External identifier (e.g. PagerDuty dedup_key, SES MessageId, webhook delivery UUID) */ - externalId?: string | null; - /** @description HTTP request headers sent to the external service */ - requestHeaders?: { - [key: string]: string | null; - } | null; - /** Format: date-time */ - attemptedAt: string; - }; - /** @description Represents an active deploy lock for a workspace */ - DeployLockDto: { - /** - * Format: uuid - * @description Unique lock identifier - */ - id: string; - /** @description Identity of the lock holder (e.g. CLI session ID, username) */ - lockedBy: string; - /** - * Format: date-time - * @description Timestamp when the lock was acquired - */ - lockedAt: string; - /** - * Format: date-time - * @description Timestamp when the lock automatically expires - */ - expiresAt: string; - }; - DiscordChannelConfig: Omit & { - /** @description Discord webhook URL */ - webhookUrl: string; - /** @description Optional Discord role ID to mention in notifications */ - mentionRoleId?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - channelType: "discord"; - }; - DnsExpectedCnameAssertion: Omit & { - /** @description Expected CNAME target the resolution must include */ - value: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_expected_cname"; - }; - DnsExpectedIpsAssertion: Omit & { - /** @description Allowed IP addresses; at least one resolved address must match */ - ips: string[]; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_expected_ips"; - }; - DnsMaxAnswersAssertion: Omit & { - /** @description DNS record type whose answer count is checked */ - recordType: string; - /** - * Format: int32 - * @description Maximum number of answers allowed for that record type - */ - max: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_max_answers"; - }; - DnsMinAnswersAssertion: Omit & { - /** @description DNS record type whose answer count is checked */ - recordType: string; - /** - * Format: int32 - * @description Minimum number of answers required for that record type - */ - min: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_min_answers"; - }; - DnsMonitorConfig: components["schemas"]["MonitorConfig"] & { - /** @description Domain name to resolve */ - hostname: string; - /** @description DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR */ - recordTypes?: ("A" | "AAAA" | "CNAME" | "MX" | "NS" | "TXT" | "SRV" | "SOA" | "CAA" | "PTR" | null)[] | null; - /** @description Custom nameservers to query (uses system defaults if omitted) */ - nameservers?: (string | null)[] | null; - /** - * Format: int32 - * @description Per-query timeout in milliseconds - */ - timeoutMs?: number | null; - /** - * Format: int32 - * @description Total timeout for all queries in milliseconds - */ - totalTimeoutMs?: number | null; - }; - DnsRecordContainsAssertion: Omit & { - /** @description DNS record type to assert on (A, AAAA, CNAME, MX, TXT) */ - recordType: string; - /** @description Substring that must appear in a matching record value */ - substring: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_record_contains"; - }; - DnsRecordEqualsAssertion: Omit & { - /** @description DNS record type to assert on (A, AAAA, CNAME, MX, TXT) */ - recordType: string; - /** @description Expected DNS record value for an exact match */ - value: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_record_equals"; - }; - DnsResolvesAssertion: Omit & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_resolves"; - }; - DnsResponseTimeAssertion: Omit & { - /** - * Format: int32 - * @description Maximum allowed DNS resolution time in milliseconds - */ - maxMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_response_time"; - }; - DnsResponseTimeWarnAssertion: Omit & { - /** - * Format: int32 - * @description DNS resolution time in milliseconds that triggers a warning only - */ - warnMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_response_time_warn"; - }; - DnsTtlHighAssertion: Omit & { - /** - * Format: int32 - * @description Maximum TTL in seconds before a high-TTL warning is raised - */ - maxTtl: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_ttl_high"; - }; - DnsTtlLowAssertion: Omit & { - /** - * Format: int32 - * @description Minimum acceptable TTL in seconds before a warning is raised - */ - minTtl: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_ttl_low"; - }; - DnsTxtContainsAssertion: Omit & { - /** @description Substring that must appear in at least one TXT record */ - substring: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "dns_txt_contains"; - }; - EmailChannelConfig: Omit & { - /** @description Email addresses to send notifications to */ - recipients: string[]; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - channelType: "email"; - }; - /** @description A single resolved entitlement for the organization */ - EntitlementDto: { - /** @description Entitlement key */ - key: string; - /** - * Format: int64 - * @description Effective limit value (overrides applied) - */ - value?: number; - /** - * Format: int64 - * @description Plan-tier default value before overrides - */ - defaultValue?: number; - /** @description Whether this entitlement has an org-level override */ - overridden?: boolean; - }; - /** @description Environment with variable substitutions for monitor configs */ - EnvironmentDto: { - /** - * Format: uuid - * @description Unique environment identifier - */ - id: string; - /** - * Format: int32 - * @description Organization this environment belongs to - */ - orgId?: number; - /** @description Human-readable environment name */ - name: string; - /** @description URL-safe identifier */ - slug: string; - /** @description Key-value variable pairs available for interpolation */ - variables: { - [key: string]: string; - }; - /** - * Format: date-time - * @description Timestamp when the environment was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the environment was last updated - */ - updatedAt: string; - /** - * Format: int32 - * @description Number of monitors using this environment - */ - monitorCount?: number; - /** @description Whether this is the default environment for new monitors */ - isDefault?: boolean; - }; - /** @description Escalation chain defining which channels to notify; null preserves current */ - EscalationChain: { - /** @description Ordered escalation steps, evaluated in sequence */ - steps: components["schemas"]["EscalationStep"][]; - /** @description Action when the incident resolves */ - onResolve?: string | null; - /** @description Action when a resolved incident reopens */ - onReopen?: string | null; - }; - /** @description Ordered escalation steps, evaluated in sequence */ - EscalationStep: { - /** - * Format: int32 - * @description Minutes to wait before executing this step (0 = immediate) - */ - delayMinutes?: number; - /** @description Alert channel IDs to notify in this step */ - channelIds: string[]; - /** @description Whether an acknowledgment is required before escalating */ - requireAck?: boolean | null; - /** - * Format: int32 - * @description Repeat notification interval in seconds until acknowledged - */ - repeatIntervalSeconds?: number | null; - }; - /** @description Details about a single monitor that failed the bulk action */ - FailureDetail: { - /** - * Format: uuid - * @description Monitor ID that failed - */ - monitorId: string; - /** @description Human-readable reason for the failure */ - reason: string; - }; - /** @description Global status summary across all subscribed vendor services */ - GlobalStatusSummaryDto: { - /** - * Format: int32 - * @description Total number of services in the catalog - */ - totalServices?: number; - /** - * Format: int32 - * @description Number of services currently fully operational - */ - operationalCount?: number; - /** - * Format: int32 - * @description Number of services with degraded status - */ - degradedCount?: number; - /** - * Format: int32 - * @description Number of services with partial outage - */ - partialOutageCount?: number; - /** - * Format: int32 - * @description Number of services with major outage - */ - majorOutageCount?: number; - /** - * Format: int32 - * @description Number of services currently under maintenance - */ - maintenanceCount?: number; - /** - * Format: int32 - * @description Number of services with unknown or null status - */ - unknownCount?: number; - /** - * Format: int64 - * @description Total number of active incidents across all services - */ - activeIncidentCount?: number; - /** @description Services that are not fully operational */ - servicesWithIssues: components["schemas"]["ServiceCatalogDto"][]; - }; - /** @description Component ordering within a single group */ - GroupComponentOrder: { - /** - * Format: uuid - * @description Group these components belong to - */ - groupId: string; - /** @description Ordered component IDs with their within-group display order */ - positions: components["schemas"]["ComponentPosition"][]; - }; - HeaderAuthConfig: Omit & { - /** @description Custom HTTP header name for the secret value */ - headerName: string; - /** - * Format: uuid - * @description Vault secret ID for the header value - */ - vaultSecretId?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "header"; - }; - HeaderValueAssertion: Omit & { - /** @description HTTP header name to assert on */ - headerName: string; - /** @description Expected value to compare against */ - expected: string; - /** - * @description Comparison operator (equals, contains, less_than, greater_than, etc.) - * @enum {string} - */ - operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "header_value"; - }; - HeartbeatIntervalDriftAssertion: Omit & { - /** - * Format: int32 - * @description Max percent drift from expected ping interval before warning (non-fatal) - */ - maxDeviationPercent: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "heartbeat_interval_drift"; - }; - HeartbeatMaxIntervalAssertion: Omit & { - /** - * Format: int32 - * @description Maximum allowed gap in seconds between consecutive heartbeat pings - */ - maxSeconds: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "heartbeat_max_interval"; - }; - HeartbeatMonitorConfig: components["schemas"]["MonitorConfig"] & { - /** - * Format: int32 - * @description Expected heartbeat interval in seconds - */ - expectedInterval: number; - /** - * Format: int32 - * @description Grace period in seconds before marking as down - */ - gracePeriod: number; - }; - HeartbeatPayloadContainsAssertion: Omit & { - /** @description JSONPath expression into the heartbeat ping JSON payload */ - path: string; - /** @description Expected value to compare against at that path */ - value: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "heartbeat_payload_contains"; - }; - HeartbeatReceivedAssertion: Omit & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "heartbeat_received"; - }; - HttpMonitorConfig: components["schemas"]["MonitorConfig"] & { - /** @description Target URL to send requests to */ - url: string; - /** - * @description HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD - * @enum {string} - */ - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; - /** @description Additional HTTP headers to include in requests */ - customHeaders?: { - [key: string]: string | null; - } | null; - /** @description Request body content for POST/PUT/PATCH methods */ - requestBody?: string | null; - /** @description Content-Type header value for the request body */ - contentType?: string | null; - /** @description Whether to verify TLS certificates (default: true) */ - verifyTls?: boolean | null; - }; - IcmpMonitorConfig: components["schemas"]["MonitorConfig"] & { - /** @description Target hostname or IP address to ping */ - host: string; - /** - * Format: int32 - * @description Number of ICMP packets to send - */ - packetCount?: number | null; - /** - * Format: int32 - * @description Ping timeout in milliseconds - */ - timeoutMs?: number | null; - }; - IcmpPacketLossAssertion: Omit & { - /** - * Format: double - * @description Maximum allowed packet loss percentage before the check fails (0–100) - */ - maxPercent: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "icmp_packet_loss"; - }; - IcmpReachableAssertion: Omit & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "icmp_reachable"; - }; - IcmpResponseTimeAssertion: Omit & { - /** - * Format: int32 - * @description Maximum average ICMP round-trip time in milliseconds - */ - maxMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "icmp_response_time"; - }; - IcmpResponseTimeWarnAssertion: Omit & { - /** - * Format: int32 - * @description ICMP round-trip time in milliseconds that triggers a warning only - */ - warnMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "icmp_response_time_warn"; - }; - IncidentDetailDto: { - incident: components["schemas"]["IncidentDto"]; - updates: components["schemas"]["IncidentUpdateDto"][]; - statusPageIncidents?: components["schemas"]["LinkedStatusPageIncidentDto"][] | null; - }; - /** @description Incident triggered by a monitor check failure or manual creation */ - IncidentDto: { - /** - * Format: uuid - * @description Unique incident identifier - */ - id: string; - /** - * Format: uuid - * @description Monitor that triggered the incident; null for service or manual incidents - */ - monitorId?: string | null; - /** - * Format: int32 - * @description Organization this incident belongs to - */ - organizationId?: number; - /** - * @description Incident origin: MONITOR, SERVICE, or MANUAL - * @enum {string} - */ - source: "AUTOMATIC" | "MANUAL" | "MONITORS" | "STATUS_DATA" | "RESOURCE_GROUP"; - /** - * @description Current lifecycle status (OPEN, RESOLVED, etc.) - * @enum {string} - */ - status: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED"; - /** - * @description Severity level: DOWN, DEGRADED, or MAINTENANCE - * @enum {string} - */ - severity: "DOWN" | "DEGRADED" | "MAINTENANCE"; - /** @description Short summary of the incident; null for auto-generated incidents */ - title?: string | null; - /** @description Human-readable description of the trigger rule that fired */ - triggeredByRule?: string | null; - /** @description Probe regions that observed the failure */ - affectedRegions: string[]; - /** - * Format: int32 - * @description Number of times this incident has been reopened - */ - reopenCount?: number; - /** - * Format: int32 - * @description User who created the incident (manual incidents only) - */ - createdByUserId?: number | null; - /** @description Whether this incident is visible on the status page */ - statusPageVisible?: boolean; - /** - * Format: uuid - * @description Linked vendor service incident ID; null for monitor incidents - */ - serviceIncidentId?: string | null; - /** - * Format: uuid - * @description Linked service catalog ID; null for monitor incidents - */ - serviceId?: string | null; - /** @description External reference ID (e.g. PagerDuty incident ID) */ - externalRef?: string | null; - /** @description Service components affected by this incident */ - affectedComponents?: string[] | null; - /** @description Short URL linking to the incident details */ - shortlink?: string | null; - /** - * @description How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.) - * @enum {string|null} - */ - resolutionReason?: "MANUAL" | "AUTO_RECOVERED" | "AUTO_RESOLVED" | null; - /** - * Format: date-time - * @description Timestamp when the incident was detected or created - */ - startedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the incident was confirmed (multi-region confirmation) - */ - confirmedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the incident was resolved - */ - resolvedAt?: string | null; - /** - * Format: date-time - * @description Cooldown window end; new incidents suppressed until this time - */ - cooldownUntil?: string | null; - /** - * Format: date-time - * @description Timestamp when the incident record was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the incident was last updated - */ - updatedAt: string; - /** @description Name of the associated monitor; populated on list responses */ - monitorName?: string | null; - /** @description Name of the associated service; populated on list responses */ - serviceName?: string | null; - /** @description Slug of the associated service; populated on list responses */ - serviceSlug?: string | null; - /** @description Type of the associated monitor; populated on list responses */ - monitorType?: string | null; - /** - * Format: uuid - * @description Resource group that owns this incident; null when not group-managed - */ - resourceGroupId?: string | null; - /** @description Name of the resource group; populated on list responses */ - resourceGroupName?: string | null; - }; - IncidentFilterParams: { - /** @enum {string|null} */ - status?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED" | null; - /** @enum {string|null} */ - severity?: "DOWN" | "DEGRADED" | "MAINTENANCE" | null; - /** @enum {string|null} */ - source?: "AUTOMATIC" | "MANUAL" | "MONITORS" | "STATUS_DATA" | "RESOURCE_GROUP" | null; - /** Format: uuid */ - monitorId?: string | null; - /** Format: uuid */ - serviceId?: string | null; - /** Format: uuid */ - resourceGroupId?: string | null; - /** Format: uuid */ - tagId?: string | null; - /** Format: uuid */ - environmentId?: string | null; - /** Format: date-time */ - startedFrom?: string | null; - /** Format: date-time */ - startedTo?: string | null; - /** Format: int32 */ - page: number; - /** Format: int32 */ - size: number; - }; - /** @description Incident detection, confirmation, and recovery policy for a monitor */ - IncidentPolicyDto: { - /** - * Format: uuid - * @description Unique incident policy identifier - */ - id: string; - /** - * Format: uuid - * @description Monitor this policy is attached to - */ - monitorId: string; - /** @description Array of trigger rules defining when an incident should be raised */ - triggerRules: components["schemas"]["TriggerRule"][]; - confirmation: components["schemas"]["ConfirmationPolicy"]; - recovery: components["schemas"]["RecoveryPolicy"]; - /** - * Format: date-time - * @description Timestamp when the policy was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the policy was last updated - */ - updatedAt: string; - /** - * Format: int32 - * @description Number of regions configured on the monitor (only set in internal API responses) - */ - monitorRegionCount?: number | null; - /** - * Format: int32 - * @description Monitor check frequency in seconds (only set in internal API responses) - */ - checkFrequencySeconds?: number | null; - }; - /** @description Lightweight reference to an incident overlapping this day */ - IncidentRef: { - /** - * Format: uuid - * @description Status page incident ID - */ - id: string; - /** @description Incident title */ - title: string; - /** @description Incident impact level */ - impact: string; - }; - /** @description Incident summary counters */ - IncidentsSummaryDto: { - /** Format: int64 */ - active: number; - /** Format: int64 */ - resolvedToday: number; - /** Format: double */ - mttr30d?: number | null; - }; - IncidentUpdateDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - incidentId: string; - /** @enum {string|null} */ - oldStatus?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED" | null; - /** @enum {string|null} */ - newStatus?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED" | null; - body?: string | null; - /** @enum {string|null} */ - createdBy?: "SYSTEM" | "USER" | null; - notifySubscribers?: boolean; - /** Format: date-time */ - createdAt: string; - }; - IntegrationConfigSchemaDto: { - connectionFields: components["schemas"]["IntegrationFieldDto"][]; - channelFields: components["schemas"]["IntegrationFieldDto"][]; - }; - IntegrationDto: { - type: string; - name: string; - description: string; - logoUrl: string; - authType: string; - /** @enum {string} */ - tierAvailability: "FREE" | "STARTER" | "PRO" | "TEAM" | "BUSINESS" | "ENTERPRISE"; - lifecycle: string; - setupGuideUrl: string; - configSchema: components["schemas"]["IntegrationConfigSchemaDto"]; - }; - IntegrationFieldDto: { - key: string; - label: string; - type: string; - required: boolean; - sensitive: boolean; - placeholder?: string | null; - helpText?: string | null; - options?: string[] | null; - default?: string | null; - }; - /** @description Organization invite sent to an email address */ - InviteDto: { - /** - * Format: int32 - * @description Unique invite identifier - */ - inviteId?: number; - /** @description Email address the invite was sent to */ - email: string; - /** - * @description Role that will be assigned to the invitee on acceptance - * @enum {string} - */ - roleOffered: "OWNER" | "ADMIN" | "MEMBER"; - /** - * Format: date-time - * @description Timestamp when the invite expires - */ - expiresAt: string; - /** - * Format: date-time - * @description Timestamp when the invite was accepted; null if not yet used - */ - consumedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the invite was revoked; null if active - */ - revokedAt?: string | null; - }; - JsonPathAssertion: Omit & { - /** @description JSONPath expression to extract a value from the response body */ - path: string; - /** @description Expected value to compare against */ - expected: string; - /** - * @description Comparison operator (equals, contains, less_than, greater_than, etc.) - * @enum {string} - */ - operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "json_path"; - }; - /** @description API key metadata */ - KeyInfo: { - /** - * Format: int32 - * @description Key ID - */ - id?: number; - /** @description Human-readable key name */ - name: string; - /** - * Format: date-time - * @description When the key was created - */ - createdAt: string; - /** - * Format: date-time - * @description When the key expires (null = never) - */ - expiresAt?: string | null; - /** - * Format: date-time - * @description Last time the key was used - */ - lastUsedAt?: string | null; - }; - LinkedStatusPageIncidentDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - statusPageId: string; - statusPageName: string; - statusPageSlug: string; - title: string; - /** @enum {string} */ - status: "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED"; - /** @enum {string} */ - impact: "NONE" | "MINOR" | "MAJOR" | "CRITICAL"; - scheduled?: boolean; - /** Format: date-time */ - publishedAt?: string | null; - }; - /** @description A component affected by a scheduled maintenance window */ - MaintenanceComponentRef: { - /** - * Format: uuid - * @description Component identifier - */ - id: string; - /** @description Component name */ - name: string; - /** @description Component status at the time of the maintenance update */ - status: string; - }; - /** @description A status update within a scheduled maintenance lifecycle */ - MaintenanceUpdateDto: { - /** - * Format: uuid - * @description Unique update identifier - */ - id: string; - /** @description Status at the time of this update */ - status: string; - /** @description Update message from the vendor */ - body?: string | null; - /** - * Format: date-time - * @description Timestamp when this update was posted - */ - displayAt?: string | null; - }; - /** @description Scheduled maintenance window for a monitor */ - MaintenanceWindowDto: { - /** - * Format: uuid - * @description Unique maintenance window identifier - */ - id: string; - /** - * Format: uuid - * @description Monitor this window applies to; null for org-wide windows - */ - monitorId?: string | null; - /** - * Format: int32 - * @description Organization this maintenance window belongs to - */ - organizationId?: number; - /** - * Format: date-time - * @description Scheduled start of the maintenance window - */ - startsAt: string; - /** - * Format: date-time - * @description Scheduled end of the maintenance window - */ - endsAt: string; - /** @description iCal RRULE for recurring windows; null for one-time */ - repeatRule?: string | null; - /** @description Human-readable reason for the maintenance */ - reason?: string | null; - /** @description Whether alerts are suppressed during this window */ - suppressAlerts?: boolean; - /** - * Format: date-time - * @description Timestamp when the window was created - */ - createdAt: string; - }; - /** @description Match rules to evaluate (all must pass; omit or empty for catch-all) */ - MatchRule: { - /** @description Rule type, e.g. severity_gte, monitor_id_in, region_in */ - type: string; - /** @description Comparison value for single-value rules like severity_gte */ - value?: string | null; - /** @description Monitor UUIDs to match for monitor_id_in rules */ - monitorIds?: string[] | null; - /** @description Region codes to match for region_in rules */ - regions?: string[] | null; - /** @description Values list for multi-value rules like monitor_type_in */ - values?: string[] | null; - }; - McpConnectsAssertion: Omit & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_connects"; - }; - McpHasCapabilityAssertion: Omit & { - /** @description Capability name the server must advertise, e.g. tools or resources */ - capability: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_has_capability"; - }; - McpMinToolsAssertion: Omit & { - /** - * Format: int32 - * @description Minimum number of tools the server must expose - */ - min: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_min_tools"; - }; - McpProtocolVersionAssertion: Omit & { - /** @description Expected MCP protocol version string from the server handshake */ - version: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_protocol_version"; - }; - McpResponseTimeAssertion: Omit & { - /** - * Format: int32 - * @description Maximum allowed MCP check duration in milliseconds - */ - maxMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_response_time"; - }; - McpResponseTimeWarnAssertion: Omit & { - /** - * Format: int32 - * @description MCP check duration in milliseconds that triggers a warning only - */ - warnMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_response_time_warn"; - }; - McpServerMonitorConfig: components["schemas"]["MonitorConfig"] & { - /** @description Command to execute to start the MCP server */ - command: string; - /** @description Command-line arguments for the MCP server process */ - args?: (string | null)[] | null; - /** @description Environment variables to pass to the MCP server process */ - env?: { - [key: string]: string | null; - } | null; - }; - McpToolAvailableAssertion: Omit & { - /** @description MCP tool name that must appear in the server's tool list */ - toolName: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_tool_available"; - }; - McpToolCountChangedAssertion: Omit & { - /** - * Format: int32 - * @description Expected tool count; warns when the live count differs - */ - expectedCount: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "mcp_tool_count_changed"; - }; - /** @description Organization member with role and status */ - MemberDto: { - /** - * Format: int32 - * @description User identifier of the member - */ - userId?: number; - /** @description Member email address */ - email: string; - /** @description Member display name; null if not set */ - name?: string | null; - /** - * @description Member role within this organization (OWNER, ADMIN, MEMBER) - * @enum {string} - */ - orgRole: "OWNER" | "ADMIN" | "MEMBER"; - /** - * @description Membership status (ACTIVE, PENDING, SUSPENDED) - * @enum {string} - */ - status: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; - /** - * Format: date-time - * @description Timestamp when the member was added to the organization - */ - createdAt: string; - }; - MonitorAssertionDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - monitorId: string; - /** @enum {string} */ - assertionType: "status_code" | "response_time" | "body_contains" | "json_path" | "header_value" | "regex_body" | "dns_resolves" | "dns_response_time" | "dns_expected_ips" | "dns_expected_cname" | "dns_record_contains" | "dns_record_equals" | "dns_txt_contains" | "dns_min_answers" | "dns_max_answers" | "dns_response_time_warn" | "dns_ttl_low" | "dns_ttl_high" | "mcp_connects" | "mcp_response_time" | "mcp_has_capability" | "mcp_tool_available" | "mcp_min_tools" | "mcp_protocol_version" | "mcp_response_time_warn" | "mcp_tool_count_changed" | "ssl_expiry" | "response_size" | "redirect_count" | "redirect_target" | "response_time_warn" | "tcp_connects" | "tcp_response_time" | "tcp_response_time_warn" | "icmp_reachable" | "icmp_response_time" | "icmp_response_time_warn" | "icmp_packet_loss" | "heartbeat_received" | "heartbeat_max_interval" | "heartbeat_interval_drift" | "heartbeat_payload_contains"; - config: components["schemas"]["BodyContainsAssertion"] | components["schemas"]["DnsExpectedCnameAssertion"] | components["schemas"]["DnsExpectedIpsAssertion"] | components["schemas"]["DnsMaxAnswersAssertion"] | components["schemas"]["DnsMinAnswersAssertion"] | components["schemas"]["DnsRecordContainsAssertion"] | components["schemas"]["DnsRecordEqualsAssertion"] | components["schemas"]["DnsResolvesAssertion"] | components["schemas"]["DnsResponseTimeAssertion"] | components["schemas"]["DnsResponseTimeWarnAssertion"] | components["schemas"]["DnsTtlHighAssertion"] | components["schemas"]["DnsTtlLowAssertion"] | components["schemas"]["DnsTxtContainsAssertion"] | components["schemas"]["HeaderValueAssertion"] | components["schemas"]["HeartbeatIntervalDriftAssertion"] | components["schemas"]["HeartbeatMaxIntervalAssertion"] | components["schemas"]["HeartbeatPayloadContainsAssertion"] | components["schemas"]["HeartbeatReceivedAssertion"] | components["schemas"]["IcmpPacketLossAssertion"] | components["schemas"]["IcmpReachableAssertion"] | components["schemas"]["IcmpResponseTimeAssertion"] | components["schemas"]["IcmpResponseTimeWarnAssertion"] | components["schemas"]["JsonPathAssertion"] | components["schemas"]["McpConnectsAssertion"] | components["schemas"]["McpHasCapabilityAssertion"] | components["schemas"]["McpMinToolsAssertion"] | components["schemas"]["McpProtocolVersionAssertion"] | components["schemas"]["McpResponseTimeAssertion"] | components["schemas"]["McpResponseTimeWarnAssertion"] | components["schemas"]["McpToolAvailableAssertion"] | components["schemas"]["McpToolCountChangedAssertion"] | components["schemas"]["RedirectCountAssertion"] | components["schemas"]["RedirectTargetAssertion"] | components["schemas"]["RegexBodyAssertion"] | components["schemas"]["ResponseSizeAssertion"] | components["schemas"]["ResponseTimeAssertion"] | components["schemas"]["ResponseTimeWarnAssertion"] | components["schemas"]["SslExpiryAssertion"] | components["schemas"]["StatusCodeAssertion"] | components["schemas"]["TcpConnectsAssertion"] | components["schemas"]["TcpResponseTimeAssertion"] | components["schemas"]["TcpResponseTimeWarnAssertion"]; - /** @enum {string} */ - severity: "fail" | "warn"; - }; - /** @description New authentication configuration (full replacement) */ - MonitorAuthConfig: { - type: string; - }; - MonitorAuthDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - monitorId: string; - /** @enum {string} */ - authType: "bearer" | "basic" | "header" | "api_key"; - config: components["schemas"]["ApiKeyAuthConfig"] | components["schemas"]["BasicAuthConfig"] | components["schemas"]["BearerAuthConfig"] | components["schemas"]["HeaderAuthConfig"]; - }; - /** @description Protocol-specific monitor configuration */ - MonitorConfig: Record; - /** @description Full monitor representation */ - MonitorDto: { - /** - * Format: uuid - * @description Unique monitor identifier - */ - id: string; - /** - * Format: int32 - * @description Organization this monitor belongs to - */ - organizationId?: number; - /** @description Human-readable name for this monitor */ - name: string; - /** @enum {string} */ - type: "HTTP" | "DNS" | "MCP_SERVER" | "TCP" | "ICMP" | "HEARTBEAT"; - config: components["schemas"]["DnsMonitorConfig"] | components["schemas"]["HeartbeatMonitorConfig"] | components["schemas"]["HttpMonitorConfig"] | components["schemas"]["IcmpMonitorConfig"] | components["schemas"]["McpServerMonitorConfig"] | components["schemas"]["TcpMonitorConfig"]; - /** - * Format: int32 - * @description Check frequency in seconds (30–86400) - */ - frequencySeconds?: number; - /** @description Whether the monitor is active */ - enabled?: boolean; - /** @description Probe regions where checks are executed */ - regions: string[]; - /** - * @description Management source: DASHBOARD or CLI - * @enum {string} - */ - managedBy: "DASHBOARD" | "CLI" | "TERRAFORM"; - /** - * Format: date-time - * @description Timestamp when the monitor was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the monitor was last updated - */ - updatedAt: string; - /** @description Assertions evaluated against each check result; null on list responses */ - assertions?: components["schemas"]["MonitorAssertionDto"][] | null; - /** @description Tags applied to this monitor */ - tags?: components["schemas"]["TagDto"][] | null; - /** @description Heartbeat ping URL; populated for HEARTBEAT monitors only */ - pingUrl?: string | null; - environment?: components["schemas"]["Summary"] | null; - auth?: Omit | null; - incidentPolicy?: components["schemas"]["IncidentPolicyDto"] | null; - /** @description Alert channel IDs linked to this monitor; populated on single-monitor responses */ - alertChannelIds?: string[] | null; - }; - /** @description Monitors that reference this secret; null on create/update responses */ - MonitorReference: { - /** - * Format: uuid - * @description Monitor identifier - */ - id: string; - /** @description Monitor name */ - name: string; - }; - /** @description Dashboard summary counters for monitors */ - MonitorsSummaryDto: { - /** - * Format: int64 - * @description Total number of monitors in the organization - */ - total: number; - /** - * Format: int64 - * @description Number of monitors currently passing - */ - up: number; - /** - * Format: int64 - * @description Number of monitors currently failing (DOWN severity) - */ - down: number; - /** - * Format: int64 - * @description Number of monitors with degraded status - */ - degraded: number; - /** - * Format: int64 - * @description Number of disabled monitors - */ - paused: number; - /** - * Format: double - * @description Average uptime percentage across all monitors over last 24h - */ - avgUptime24h?: number | null; - /** - * Format: double - * @description Average uptime percentage across all monitors over last 30 days - */ - avgUptime30d?: number | null; - }; - MonitorTestRequest: { - /** - * @description Monitor protocol type to test - * @enum {string} - */ - type: "HTTP" | "DNS" | "MCP_SERVER" | "TCP" | "ICMP" | "HEARTBEAT"; - config: components["schemas"]["DnsMonitorConfig"] | components["schemas"]["HeartbeatMonitorConfig"] | components["schemas"]["HttpMonitorConfig"] | components["schemas"]["IcmpMonitorConfig"] | components["schemas"]["McpServerMonitorConfig"] | components["schemas"]["TcpMonitorConfig"]; - /** @description Optional assertions to evaluate against the test result */ - assertions?: components["schemas"]["CreateAssertionRequest"][] | null; - }; - MonitorTestResultDto: { - passed?: boolean; - error?: string | null; - /** Format: int32 */ - statusCode?: number | null; - /** Format: int64 */ - responseTimeMs?: number | null; - responseHeaders?: { - [key: string]: (string | null)[] | null; - } | null; - bodyPreview?: string | null; - /** Format: int64 */ - responseSizeBytes?: number | null; - /** Format: int32 */ - redirectCount?: number | null; - finalUrl?: string | null; - assertionResults: components["schemas"]["AssertionTestResultDto"][]; - warnings?: string[] | null; - }; - /** @description A point-in-time version snapshot of a monitor configuration */ - MonitorVersionDto: { - /** - * Format: uuid - * @description Unique version record identifier - */ - id: string; - /** - * Format: uuid - * @description Monitor this version belongs to - */ - monitorId: string; - /** - * Format: int32 - * @description Monotonically increasing version number - */ - version?: number; - snapshot: components["schemas"]["MonitorDto"]; - /** - * Format: int32 - * @description User ID who made the change; null for automated changes - */ - changedById?: number | null; - /** - * @description Change source (DASHBOARD, CLI, API) - * @enum {string} - */ - changedVia: "API" | "DASHBOARD" | "CLI" | "TERRAFORM"; - /** @description Human-readable description of what changed */ - changeSummary?: string | null; - /** - * Format: date-time - * @description Timestamp when this version was recorded - */ - createdAt: string; - }; - /** @description Inline tag creation — creates the tag if it does not already exist */ - NewTagRequest: { - /** @description Tag name */ - name: string; - /** @description Hex color code (defaults to #6B7280 if omitted) */ - color?: string | null; - }; - /** @description Dispatch state for a single (incident, notification policy) pair, with delivery history */ - NotificationDispatchDto: { - /** - * Format: uuid - * @description Unique dispatch record identifier - */ - id: string; - /** - * Format: uuid - * @description Incident this dispatch is for - */ - incidentId: string; - /** - * Format: uuid - * @description Notification policy that matched this incident - */ - policyId: string; - /** @description Human-readable name of the matched policy (null if policy has been deleted) */ - policyName?: string | null; - /** - * @description Current dispatch state - * @enum {string} - */ - status: "PENDING" | "DISPATCHING" | "DELIVERED" | "ESCALATING" | "ACKNOWLEDGED" | "COMPLETED"; - /** - * @description Why the dispatch reached COMPLETED: EXHAUSTED (all steps ran, no ack), RESOLVED (incident resolved), NO_STEPS (policy had no steps). Null for non-terminal states. - * @enum {string|null} - */ - completionReason?: "EXHAUSTED" | "RESOLVED" | "NO_STEPS" | null; - /** - * Format: int32 - * @description 1-based index of the currently active escalation step - */ - currentStep?: number; - /** - * Format: int32 - * @description Total number of escalation steps in the policy (null if policy has been deleted) - */ - totalSteps?: number | null; - /** - * Format: date-time - * @description Timestamp when this dispatch was acknowledged (null if not acknowledged) - */ - acknowledgedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the next escalation step will fire (null if not scheduled) - */ - nextEscalationAt?: string | null; - /** - * Format: date-time - * @description Timestamp of the most recent notification delivery - */ - lastNotifiedAt?: string | null; - /** @description Delivery records for all channels associated with this dispatch */ - deliveries: components["schemas"]["AlertDeliveryDto"][]; - /** - * Format: date-time - * @description Timestamp when the dispatch was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the dispatch was last updated - */ - updatedAt: string; - }; - /** @description In-app notification for the current user */ - NotificationDto: { - /** - * Format: int64 - * @description Unique notification identifier - */ - id?: number; - /** @description Notification category (e.g. incident, monitor, team) */ - type: string; - /** @description Short notification title */ - title: string; - /** @description Full notification body; null for title-only notifications */ - body?: string | null; - /** @description Type of the resource this notification is about */ - resourceType?: string | null; - /** @description ID of the resource this notification is about */ - resourceId?: string | null; - /** @description Whether the notification has been read */ - read?: boolean; - /** - * Format: date-time - * @description Timestamp when the notification was created - */ - createdAt: string; - }; - /** @description Org-level notification policy with match rules and escalation chain */ - NotificationPolicyDto: { - /** - * Format: uuid - * @description Unique notification policy identifier - */ - id: string; - /** - * Format: int32 - * @description Organization this policy belongs to - */ - organizationId?: number; - /** @description Human-readable name for this policy */ - name: string; - /** @description Match rules (all must pass; empty = catch-all) */ - matchRules: components["schemas"]["MatchRule"][]; - escalation: components["schemas"]["EscalationChain"]; - /** @description Whether this policy is active */ - enabled?: boolean; - /** - * Format: int32 - * @description Evaluation order; higher value = evaluated first - */ - priority?: number; - /** - * Format: date-time - * @description Timestamp when the policy was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the policy was last updated - */ - updatedAt: string; - }; - OpsGenieChannelConfig: Omit & { - /** @description OpsGenie API key for alert creation */ - apiKey: string; - /** @description OpsGenie API region: us or eu */ - region?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - channelType: "opsgenie"; - }; - /** @description Organization account details */ - OrganizationDto: { - /** - * Format: int32 - * @description Unique organization identifier - */ - id?: number; - /** @description Organization name */ - name: string; - /** @description Billing and contact email */ - email: string | null; - /** @description Team size range (e.g. 1-10, 11-50) */ - size?: string | null; - /** @description Industry vertical (e.g. SaaS, Fintech) */ - industry?: string | null; - /** @description Organization website URL */ - websiteUrl?: string | null; - }; - /** @description Organization the key belongs to */ - OrgInfo: { - /** - * Format: int32 - * @description Organization ID - */ - id?: number; - /** @description Organization name */ - name: string; - }; - Pageable: { - /** Format: int32 */ - page: number; - /** Format: int32 */ - size: number; - sort: string[]; - }; - PagerDutyChannelConfig: Omit & { - /** @description PagerDuty Events API v2 routing (integration) key */ - routingKey: string; - /** @description Override PagerDuty severity mapping */ - severityOverride?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - channelType: "pagerduty"; - }; - /** @description A top-level page section (either a group or an ungrouped component) */ - PageSection: { - /** - * Format: uuid - * @description Group ID when this section is a group - */ - groupId?: string | null; - /** - * Format: uuid - * @description Component ID when this section is an ungrouped component - */ - componentId?: string | null; - /** - * Format: int32 - * @description Position on the page (0-based) - */ - pageOrder: number; - }; - /** @description Billing plan and entitlement state */ - PlanInfo: { - /** - * @description Resolved plan tier - * @enum {string} - */ - tier: "FREE" | "STARTER" | "PRO" | "TEAM" | "BUSINESS" | "ENTERPRISE"; - /** @description Subscription status (null if no subscription) */ - subscriptionStatus?: string | null; - /** @description Whether the org is on a trial */ - trialActive?: boolean; - /** - * Format: date-time - * @description Trial expiry (null if not trialing) - */ - trialExpiresAt?: string | null; - /** @description Entitlement limits keyed by entitlement name */ - entitlements: { - [key: string]: components["schemas"]["EntitlementDto"]; - }; - /** @description Current usage counters keyed by entitlement name */ - usage: { - [key: string]: number; - }; - }; - /** @description Aggregated poll metrics for a time bucket */ - PollChartBucketDto: { - /** - * Format: date-time - * @description Start of the time bucket (ISO 8601) - */ - bucket: string; - /** - * Format: double - * @description Uptime percentage for this bucket; null when no data - * @example 100 - */ - uptimePercent?: number | null; - /** - * Format: double - * @description Average response time in milliseconds for this bucket - * @example 245.3 - */ - avgResponseTimeMs?: number | null; - /** - * Format: int64 - * @description Total polls in this bucket - * @example 60 - */ - totalPolls?: number; - }; - PublishStatusPageIncidentRequest: { - /** @description Customer-facing title; null keeps draft value */ - title?: string | null; - /** - * @description Impact level; null keeps draft value - * @enum {string|null} - */ - impact?: "NONE" | "MINOR" | "MAJOR" | "CRITICAL" | null; - /** - * @description Incident status; null keeps draft value (must be an active status) - * @enum {string|null} - */ - status?: "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED" | null; - /** @description Initial update body; null keeps draft value */ - body?: string | null; - /** @description Affected components; null keeps draft value */ - affectedComponents?: components["schemas"]["AffectedComponent"][] | null; - /** @description Whether to notify subscribers (default: true) */ - notifySubscribers?: boolean | null; - }; - /** @description Rate-limit quota for the current sliding window */ - RateLimitInfo: { - /** - * Format: int64 - * @description Maximum requests allowed per window - */ - requestsPerMinute: number; - /** - * Format: int64 - * @description Requests remaining in the current window - */ - remaining: number; - /** - * Format: int64 - * @description Sliding window size in milliseconds - */ - windowMs: number; - }; - /** @description Auto-recovery settings */ - RecoveryPolicy: { - /** - * Format: int32 - * @description Consecutive passing checks required to auto-resolve the incident - */ - consecutiveSuccesses: number; - /** - * Format: int32 - * @description Minimum regions that must be passing before recovery can complete - */ - minRegionsPassing: number; - /** - * Format: int32 - * @description Minutes after resolve before a new incident may open on the same monitor - */ - cooldownMinutes: number; - }; - RedirectCountAssertion: Omit & { - /** - * Format: int32 - * @description Maximum number of HTTP redirects allowed before the check fails - */ - maxCount: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "redirect_count"; - }; - RedirectTargetAssertion: Omit & { - /** @description Expected final URL after following redirects */ - expected: string; - /** - * @description Comparison operator (equals, contains, less_than, greater_than, etc.) - * @enum {string} - */ - operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "redirect_target"; - }; - RegexBodyAssertion: Omit & { - /** @description Regular expression the response body must match */ - pattern: string; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "regex_body"; - }; - /** @description Latest check result for a single region */ - RegionStatusDto: { - /** - * @description Region identifier - * @example us-east - */ - region: string; - /** - * @description Whether the last check in this region passed - * @example true - */ - passed?: boolean; - /** - * Format: int32 - * @description Response time in milliseconds for the last check - * @example 95 - */ - responseTimeMs?: number | null; - /** - * Format: date-time - * @description Timestamp of the last check in this region (ISO 8601) - */ - timestamp: string; - /** @description Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing */ - severityHint?: string | null; - }; - /** @description Request body for removing tags from a monitor */ - RemoveMonitorTagsRequest: { - /** @description IDs of the tags to detach from the monitor */ - tagIds: string[]; - }; - /** @description Batch component reorder request */ - ReorderComponentsRequest: { - /** @description Ordered list of component IDs with their new positions */ - positions: components["schemas"]["ComponentPosition"][]; - }; - /** @description Reorder page-level layout: groups and ungrouped components share one ordering */ - ReorderPageLayoutRequest: { - /** @description Top-level sections in their new order */ - sections: components["schemas"]["PageSection"][]; - /** @description Within-group component ordering; only needed for groups whose internal order changed */ - groupOrders?: components["schemas"]["GroupComponentOrder"][] | null; - }; - ResolveIncidentRequest: { - /** @description Optional resolution message or post-mortem notes */ - body: string; - }; - /** @description Resource group with health summary and optional member details */ - ResourceGroupDto: { - /** - * Format: uuid - * @description Unique resource group identifier - */ - id: string; - /** - * Format: int32 - * @description Organization this group belongs to - */ - organizationId?: number; - /** @description Human-readable group name */ - name: string; - /** @description URL-safe group identifier */ - slug: string; - /** @description Optional group description */ - description?: string | null; - /** - * Format: uuid - * @description Notification policy applied to this group - */ - alertPolicyId?: string | null; - /** - * Format: int32 - * @description Default check frequency in seconds for member monitors - */ - defaultFrequency?: number | null; - /** @description Default regions for member monitors */ - defaultRegions?: string[] | null; - defaultRetryStrategy?: components["schemas"]["RetryStrategy"] | null; - /** @description Default alert channel IDs for member monitors */ - defaultAlertChannels?: string[] | null; - /** - * Format: uuid - * @description Default environment ID for member monitors - */ - defaultEnvironmentId?: string | null; - /** - * @description Health threshold type: COUNT or PERCENTAGE - * @enum {string|null} - */ - healthThresholdType?: "COUNT" | "PERCENTAGE" | null; - /** @description Health threshold value */ - healthThresholdValue?: number | null; - /** @description When true, member-level incidents skip notification dispatch; only group alerts fire */ - suppressMemberAlerts?: boolean; - /** - * Format: int32 - * @description Seconds to wait after health threshold breach before creating group incident - */ - confirmationDelaySeconds?: number | null; - /** - * Format: int32 - * @description Cooldown minutes after group incident resolves before a new one can open - */ - recoveryCooldownMinutes?: number | null; - health: components["schemas"]["ResourceGroupHealthDto"]; - /** @description Member list with individual statuses; populated on detail GET only */ - members?: components["schemas"]["ResourceGroupMemberDto"][] | null; - /** - * Format: date-time - * @description Timestamp when the group was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the group was last updated - */ - updatedAt: string; - }; - /** @description Aggregated health summary for a resource group */ - ResourceGroupHealthDto: { - /** - * @description Worst-of health status across all members - * @enum {string} - */ - status: "operational" | "maintenance" | "degraded" | "down"; - /** - * Format: int32 - * @description Total number of members in the group - */ - totalMembers?: number; - /** - * Format: int32 - * @description Number of members currently in operational status - */ - operationalCount?: number; - /** - * Format: int32 - * @description Number of members with an active incident or non-operational status - */ - activeIncidents?: number; - /** - * @description Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured. - * @enum {string|null} - */ - thresholdStatus?: "healthy" | "degraded" | "down" | null; - /** - * Format: int32 - * @description Number of failing members at time of last evaluation - */ - failingCount?: number | null; - }; - /** @description A single member of a resource group with its computed health status */ - ResourceGroupMemberDto: { - /** - * Format: uuid - * @description Unique group member record identifier - */ - id: string; - /** - * Format: uuid - * @description Resource group this member belongs to - */ - groupId: string; - /** @description Type of member: 'monitor' or 'service' */ - memberType: string; - /** - * Format: uuid - * @description Monitor ID; set when memberType is 'monitor' - */ - monitorId?: string | null; - /** - * Format: uuid - * @description Service ID; set when memberType is 'service' - */ - serviceId?: string | null; - /** @description Display name of the referenced monitor or service */ - name?: string | null; - /** @description Slug identifier for the service (services only); used for icons and uptime API calls */ - slug?: string | null; - /** - * Format: uuid - * @description Subscription ID for the service (services only); used to link to the dependency detail page - */ - subscriptionId?: string | null; - /** - * @description Computed health status for this member - * @enum {string} - */ - status: "operational" | "maintenance" | "degraded" | "down"; - /** @description Effective check frequency label showing the group default when the monitor inherits it; null for services or when no group default is configured */ - effectiveFrequency?: string | null; - /** - * Format: date-time - * @description Timestamp when the member was added to the group - */ - createdAt: string; - /** - * Format: double - * @description 24h uptime percentage; populated when includeMetrics=true - */ - uptime24h?: number | null; - /** @description Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true */ - chartData?: number[] | null; - /** - * Format: double - * @description Average latency in ms (monitors only); populated when includeMetrics=true - */ - avgLatencyMs?: number | null; - /** - * Format: double - * @description P95 latency in ms (monitors only); populated when includeMetrics=true - */ - p95LatencyMs?: number | null; - /** - * Format: date-time - * @description Timestamp of the most recent health check; populated when includeMetrics=true - */ - lastCheckedAt?: string | null; - /** @description Monitor type (HTTP, DNS, TCP, ICMP, HEARTBEAT, MCP); monitors only */ - monitorType?: string | null; - /** @description Environment name; monitors only */ - environmentName?: string | null; - }; - ResponseSizeAssertion: Omit & { - /** - * Format: int32 - * @description Maximum response body size in bytes before the check fails - */ - maxBytes: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "response_size"; - }; - ResponseTimeAssertion: Omit & { - /** - * Format: int32 - * @description Maximum allowed response time in milliseconds before the check fails - */ - thresholdMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "response_time"; - }; - ResponseTimeWarnAssertion: Omit & { - /** - * Format: int32 - * @description HTTP response time in milliseconds that triggers a warning only - */ - warnMs: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "response_time_warn"; - }; - /** @description Dashboard summary: current status, per-region latest results, and chart data */ - ResultSummaryDto: { - /** - * @description Derived current status across all regions - * @enum {string} - */ - currentStatus: "up" | "degraded" | "down" | "unknown"; - /** @description Latest check result per region */ - latestPerRegion: components["schemas"]["RegionStatusDto"][]; - /** @description Time-bucketed chart data for the requested window */ - chartData: components["schemas"]["ChartBucketDto"][]; - /** - * Format: double - * @description Uptime percentage over the last 24 hours; null when no data - * @example 99.95 - */ - uptime24h?: number | null; - /** - * Format: double - * @description Uptime percentage for the selected chart window; null when no data - * @example 99.8 - */ - uptimeWindow?: number | null; - }; - /** @description Default retry strategy for member monitors; null clears */ - RetryStrategy: { - /** @description Retry strategy kind, e.g. fixed interval between attempts */ - type: string; - /** - * Format: int32 - * @description Maximum number of retries after a failed check - */ - maxRetries?: number; - /** - * Format: int32 - * @description Delay between retry attempts in seconds - */ - interval?: number; - }; - /** @description A scheduled maintenance window from a vendor status page */ - ScheduledMaintenanceDto: { - /** - * Format: uuid - * @description Unique maintenance record identifier - */ - id: string; - /** @description Vendor-assigned maintenance identifier */ - externalId: string; - /** @description Maintenance title as reported by the vendor */ - title: string; - /** @description Current maintenance status (scheduled, in_progress, completed) */ - status: string; - /** @description Reported impact level */ - impact?: string | null; - /** @description Vendor-provided short URL to the maintenance page */ - shortlink?: string | null; - /** - * Format: date-time - * @description Timestamp when the maintenance is scheduled to begin - */ - scheduledFor?: string | null; - /** - * Format: date-time - * @description Timestamp when the maintenance is scheduled to end - */ - scheduledUntil?: string | null; - /** - * Format: date-time - * @description Timestamp when the maintenance actually started - */ - startedAt?: string | null; - /** - * Format: date-time - * @description Timestamp when the maintenance was completed - */ - completedAt?: string | null; - /** @description Components affected by this maintenance */ - affectedComponents: components["schemas"]["MaintenanceComponentRef"][]; - /** @description Status updates posted during the maintenance lifecycle */ - updates: components["schemas"]["MaintenanceUpdateDto"][]; - }; - /** @description Secret with change-detection hash; plaintext value is never returned */ - SecretDto: { - /** - * Format: uuid - * @description Unique secret identifier - */ - id: string; - /** @description Secret key name, unique within the workspace */ - key: string; - /** - * Format: int32 - * @description DEK version at the time of last encryption - */ - dekVersion?: number; - /** @description SHA-256 hex digest of the current plaintext; use for change detection */ - valueHash: string; - /** - * Format: date-time - * @description Timestamp when the secret was created - */ - createdAt: string; - /** - * Format: date-time - * @description Timestamp when the secret was last updated - */ - updatedAt: string; - /** @description Monitors that reference this secret; null on create/update responses */ - usedByMonitors?: components["schemas"]["MonitorReference"][] | null; - }; - /** @description Admin-editable SEO metadata for pSEO pages */ - SeoMetadataDto: { - /** @description Short description for meta tags (max 160 chars) */ - shortDescription?: string | null; - /** @description Full description for the service page */ - description?: string | null; - /** @description Long-form about text for the About section on pSEO pages */ - about?: string | null; - }; - /** @description Related services */ - ServiceCatalogDto: { - /** Format: uuid */ - id: string; - slug: string; - name: string; - category?: string | null; - officialStatusUrl?: string | null; - developerContext?: string | null; - logoUrl?: string | null; - adapterType: string; - /** Format: int32 */ - pollingIntervalSeconds?: number; - enabled?: boolean; - published?: boolean; - overallStatus?: string | null; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - /** Format: int64 */ - componentCount?: number; - /** Format: int64 */ - activeIncidentCount?: number; - dataCompleteness: string; - /** - * Format: double - * @description Aggregated 30-day uptime percentage across all components - */ - uptime30d?: number | null; - }; - /** @description A first-class service component with lifecycle and uptime data */ - ServiceComponentDto: { - /** Format: uuid */ - id: string; - externalId: string; - name: string; - status: string; - description?: string | null; - /** Format: uuid */ - groupId?: string | null; - /** Format: int32 */ - position?: number | null; - showcase?: boolean; - onlyShowIfDegraded?: boolean; - /** Format: date-time */ - startDate?: string | null; - /** Format: date-time */ - vendorCreatedAt?: string | null; - lifecycleStatus: string; - /** - * @description Data classification: full, status_only, or metric_only - * @example full - */ - dataType: string; - /** @description Whether uptime data is available for this component */ - hasUptime?: boolean; - /** @description Geographic region for regional components (AWS, GCP, Azure) */ - region?: string | null; - /** @description Display name of the parent group */ - groupName?: string | null; - /** @description Group-only: render an aggregated uptime bar above this group's children */ - displayAggregatedUptime?: boolean; - /** - * Format: int32 - * @description Group-only count of visible leaf children; null for leaves - */ - childCount?: number | null; - uptime?: components["schemas"]["ComponentUptimeSummaryDto"] | null; - /** Format: date-time */ - statusChangedAt?: string | null; - /** Format: date-time */ - firstSeenAt: string; - /** Format: date-time */ - lastSeenAt: string; - isGroup?: boolean; - }; - /** @description One-day rollup for a public service status page: aggregated uptime, per-component impact, and incidents that overlapped the day. Powers the click/hover-to-expand panel under each uptime bar. */ - ServiceDayDetailDto: { - /** - * Format: date - * @description UTC calendar day this rollup covers - */ - date: string; - /** - * Format: double - * @description Average uptime % across leaf components with uptime data; null if no data - */ - overallUptimePercentage?: number | null; - /** - * Format: int64 - * @description Sum of partial outage seconds across all leaf components - */ - totalPartialOutageSeconds?: number; - /** - * Format: int64 - * @description Sum of major outage seconds across all leaf components - */ - totalMajorOutageSeconds?: number; - /** @description Per-component impact rows for the day (only components with uptime data) */ - components: components["schemas"]["ComponentImpact"][]; - /** @description Incidents that were active at any point during this day (started before day end, resolved after day start) */ - incidents: components["schemas"]["DayIncident"][]; - }; - ServiceDetailDto: { - /** Format: uuid */ - id: string; - slug: string; - name: string; - category?: string | null; - officialStatusUrl?: string | null; - developerContext?: string | null; - logoUrl?: string | null; - adapterType: string; - /** Format: int32 */ - pollingIntervalSeconds?: number; - enabled?: boolean; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - currentStatus?: components["schemas"]["ServiceStatusDto"] | null; - recentIncidents: components["schemas"]["ServiceIncidentDto"][]; - components: components["schemas"]["ServiceComponentDto"][]; - uptime?: components["schemas"]["ComponentUptimeSummaryDto"] | null; - activeMaintenances: components["schemas"]["ScheduledMaintenanceDto"][]; - dataCompleteness: string; - seoMetadata?: components["schemas"]["SeoMetadataDto"] | null; - relatedServices?: components["schemas"]["ServiceCatalogDto"][] | null; - }; - ServiceIncidentDetailDto: { - /** Format: uuid */ - id: string; - title: string; - status: string; - impact?: string | null; - /** Format: date-time */ - startedAt?: string | null; - /** Format: date-time */ - resolvedAt?: string | null; - /** Format: date-time */ - detectedAt?: string | null; - shortlink?: string | null; - affectedComponents?: string[] | null; - updates: components["schemas"]["ServiceIncidentUpdateDto"][]; - }; - ServiceIncidentDto: { - /** Format: uuid */ - id: string; - /** Format: uuid */ - serviceId: string; - serviceSlug?: string | null; - serviceName?: string | null; - externalId?: string | null; - title: string; - status: string; - impact?: string | null; - /** Format: date-time */ - startedAt?: string | null; - /** Format: date-time */ - resolvedAt?: string | null; - /** Format: date-time */ - updatedAt?: string | null; - shortlink?: string | null; - /** Format: date-time */ - detectedAt?: string | null; - /** Format: date-time */ - vendorCreatedAt?: string | null; - }; - ServiceIncidentUpdateDto: { - status: string; - body?: string | null; - /** Format: date-time */ - displayAt?: string | null; - }; - ServiceLiveStatusDto: { - /** @description Current overall status of the service, e.g. operational, degraded_performance */ - overallStatus?: string | null; - /** @description Current status of each active component */ - componentStatuses: components["schemas"]["ComponentStatusDto"][]; - /** - * Format: int32 - * @description Number of currently unresolved incidents - */ - activeIncidentCount?: number; - /** @description ISO 8601 timestamp of the last status poll */ - lastPolledAt?: string | null; - }; - /** @description A single poll result from the status poller */ - ServicePollResultDto: { - /** - * Format: uuid - * @description Service ID - */ - serviceId: string; - /** - * Format: date-time - * @description Timestamp when the poll was executed (ISO 8601) - */ - timestamp: string; - /** - * @description Overall status of the service at time of poll - * @example operational - */ - overallStatus?: string | null; - /** - * Format: int32 - * @description Response time of the poll in milliseconds - * @example 245 - */ - responseTimeMs?: number | null; - /** - * Format: int32 - * @description HTTP status code from the upstream status page - * @example 200 - */ - httpStatusCode?: number | null; - /** - * @description Whether the poll succeeded - * @example true - */ - passed?: boolean; - /** @description Reason for failure when passed=false */ - failureReason?: string | null; - /** - * Format: int32 - * @description Number of components reported by the service - * @example 12 - */ - componentCount?: number; - /** - * Format: int32 - * @description Number of degraded or non-operational components - * @example 1 - */ - degradedCount?: number; - }; - /** @description Aggregated poll metrics and chart data for a service */ - ServicePollSummaryDto: { - /** - * Format: double - * @description Uptime percentage over the requested window; null when no data - * @example 99.95 - */ - uptimePercentage?: number | null; - /** - * Format: int64 - * @description Total number of polls executed - * @example 4320 - */ - totalPolls?: number; - /** - * Format: int64 - * @description Number of polls that succeeded - * @example 4318 - */ - passedPolls?: number; - /** - * Format: double - * @description Average response time in milliseconds; null when no data - * @example 312.5 - */ - avgResponseTimeMs?: number | null; - /** - * Format: double - * @description 95th-percentile response time in milliseconds; null when no data - * @example 580 - */ - p95ResponseTimeMs?: number | null; - /** - * @description Time window used for the summary - * @example 30d - */ - window: string; - /** @description Time-bucketed chart data for response time and uptime */ - chartData: components["schemas"]["PollChartBucketDto"][]; - }; - ServiceStatusDto: { - overallStatus: string; - /** Format: date-time */ - lastPolledAt?: string | null; - }; - /** @description Optional body for subscribing to a specific component of a service */ - ServiceSubscribeRequest: { - /** - * Format: uuid - * @description ID of the component to subscribe to. Omit or null for whole-service subscription. - */ - componentId?: string | null; - /** @description Alert sensitivity level. Defaults to INCIDENTS_ONLY when not provided. */ - alertSensitivity?: string | null; - }; - /** @description An org-level service subscription with current status information */ - ServiceSubscriptionDto: { - /** - * Format: uuid - * @description Unique subscription identifier - */ - subscriptionId: string; - /** - * Format: uuid - * @description Service identifier - */ - serviceId: string; - slug: string; - name: string; - category?: string | null; - officialStatusUrl?: string | null; - adapterType: string; - /** Format: int32 */ - pollingIntervalSeconds?: number; - enabled?: boolean; - /** @description Logo URL from the service catalog */ - logoUrl?: string | null; - /** @description Current overall status; null when the service has never been polled */ - overallStatus?: string | null; - /** - * Format: uuid - * @description Subscribed component id; null for whole-service subscription - */ - componentId?: string | null; - component?: components["schemas"]["ServiceComponentDto"] | null; - /** - * @description Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity) - * @enum {string} - */ - alertSensitivity: "ALL" | "INCIDENTS_ONLY" | "MAJOR_ONLY"; - /** - * Format: date-time - * @description When the organization subscribed to this service - */ - subscribedAt: string; - }; - /** @description Uptime response with per-bucket breakdown and overall percentage for the period */ - ServiceUptimeResponse: { - /** - * Format: double - * @description Overall uptime percentage across the entire period; null when no polling data exists - * @example 99.95 - */ - overallUptimePct?: number | null; - /** - * @description Requested period - * @example 7d - */ - period: string; - /** - * @description Requested granularity - * @example hourly - */ - granularity: string; - /** @description Per-bucket breakdown ordered by time ascending */ - buckets: components["schemas"]["UptimeBucketDto"][]; - /** - * @description Data source: vendor_reported, incident_derived, or poll_derived - * @example vendor_reported - */ - source?: string | null; - }; - /** @description Replace the alert channels linked to a monitor */ - SetAlertChannelsRequest: { - /** @description IDs of alert channels to link (replaces current list) */ - channelIds: string[]; - }; - SetMonitorAuthRequest: { - config: components["schemas"]["ApiKeyAuthConfig"] | components["schemas"]["BasicAuthConfig"] | components["schemas"]["BearerAuthConfig"] | components["schemas"]["HeaderAuthConfig"]; - }; - SingleValueResponseAlertChannelDto: { - data: components["schemas"]["AlertChannelDto"]; - }; - SingleValueResponseAlertDeliveryDto: { - data: components["schemas"]["AlertDeliveryDto"]; - }; - SingleValueResponseApiKeyCreateResponse: { - data: components["schemas"]["ApiKeyCreateResponse"]; - }; - SingleValueResponseApiKeyDto: { - data: components["schemas"]["ApiKeyDto"]; - }; - SingleValueResponseAuthMeResponse: { - data: components["schemas"]["AuthMeResponse"]; - }; - SingleValueResponseBulkMonitorActionResult: { - data: components["schemas"]["BulkMonitorActionResult"]; - }; - SingleValueResponseDashboardOverviewDto: { - data: components["schemas"]["DashboardOverviewDto"]; - }; - SingleValueResponseDekRotationResultDto: { - data: components["schemas"]["DekRotationResultDto"]; - }; - SingleValueResponseDeployLockDto: { - data: components["schemas"]["DeployLockDto"]; - }; - SingleValueResponseEnvironmentDto: { - data: components["schemas"]["EnvironmentDto"]; - }; - SingleValueResponseGlobalStatusSummaryDto: { - data: components["schemas"]["GlobalStatusSummaryDto"]; - }; - SingleValueResponseIncidentDetailDto: { - data: components["schemas"]["IncidentDetailDto"]; - }; - SingleValueResponseIncidentPolicyDto: { - data: components["schemas"]["IncidentPolicyDto"]; - }; - SingleValueResponseInviteDto: { - data: components["schemas"]["InviteDto"]; - }; - SingleValueResponseListUUID: { - data: string[]; - }; - SingleValueResponseLong: { - /** Format: int64 */ - data: number; - }; - SingleValueResponseMaintenanceWindowDto: { - data: components["schemas"]["MaintenanceWindowDto"]; - }; - SingleValueResponseMapStringListComponentUptimeDayDto: { - data: { - [key: string]: components["schemas"]["ComponentUptimeDayDto"][]; - }; - }; - SingleValueResponseMonitorAssertionDto: { - data: components["schemas"]["MonitorAssertionDto"]; - }; - SingleValueResponseMonitorAuthDto: { - data: components["schemas"]["MonitorAuthDto"]; - }; - SingleValueResponseMonitorDto: { - data: components["schemas"]["MonitorDto"]; - }; - SingleValueResponseMonitorTestResultDto: { - data: components["schemas"]["MonitorTestResultDto"]; - }; - SingleValueResponseMonitorVersionDto: { - data: components["schemas"]["MonitorVersionDto"]; - }; - SingleValueResponseNotificationDispatchDto: { - data: components["schemas"]["NotificationDispatchDto"]; - }; - SingleValueResponseNotificationPolicyDto: { - data: components["schemas"]["NotificationPolicyDto"]; - }; - SingleValueResponseOrganizationDto: { - data: components["schemas"]["OrganizationDto"]; - }; - SingleValueResponseResourceGroupDto: { - data: components["schemas"]["ResourceGroupDto"]; - }; - SingleValueResponseResourceGroupHealthDto: { - data: components["schemas"]["ResourceGroupHealthDto"]; - }; - SingleValueResponseResourceGroupMemberDto: { - data: components["schemas"]["ResourceGroupMemberDto"]; - }; - SingleValueResponseResultSummaryDto: { - data: components["schemas"]["ResultSummaryDto"]; - }; - SingleValueResponseSecretDto: { - data: components["schemas"]["SecretDto"]; - }; - SingleValueResponseServiceDayDetailDto: { - data: components["schemas"]["ServiceDayDetailDto"]; - }; - SingleValueResponseServiceDetailDto: { - data: components["schemas"]["ServiceDetailDto"]; - }; - SingleValueResponseServiceIncidentDetailDto: { - data: components["schemas"]["ServiceIncidentDetailDto"]; - }; - SingleValueResponseServiceLiveStatusDto: { - data: components["schemas"]["ServiceLiveStatusDto"]; - }; - SingleValueResponseServicePollSummaryDto: { - data: components["schemas"]["ServicePollSummaryDto"]; - }; - SingleValueResponseServiceSubscriptionDto: { - data: components["schemas"]["ServiceSubscriptionDto"]; - }; - SingleValueResponseServiceUptimeResponse: { - data: components["schemas"]["ServiceUptimeResponse"]; - }; - SingleValueResponseStatusPageComponentDto: { - data: components["schemas"]["StatusPageComponentDto"]; - }; - SingleValueResponseStatusPageComponentGroupDto: { - data: components["schemas"]["StatusPageComponentGroupDto"]; - }; - SingleValueResponseStatusPageCustomDomainDto: { - data: components["schemas"]["StatusPageCustomDomainDto"]; - }; - SingleValueResponseStatusPageDto: { - data: components["schemas"]["StatusPageDto"]; - }; - SingleValueResponseStatusPageIncidentDto: { - data: components["schemas"]["StatusPageIncidentDto"]; - }; - SingleValueResponseStatusPageSubscriberDto: { - data: components["schemas"]["StatusPageSubscriberDto"]; - }; - SingleValueResponseString: { - data: string; - }; - SingleValueResponseTagDto: { - data: components["schemas"]["TagDto"]; - }; - SingleValueResponseTestChannelResult: { - data: components["schemas"]["TestChannelResult"]; - }; - SingleValueResponseTestMatchResult: { - data: components["schemas"]["TestMatchResult"]; - }; - SingleValueResponseUptimeDto: { - data: components["schemas"]["UptimeDto"]; - }; - SingleValueResponseWebhookEndpointDto: { - data: components["schemas"]["WebhookEndpointDto"]; - }; - SingleValueResponseWebhookSigningSecretDto: { - data: components["schemas"]["WebhookSigningSecretDto"]; - }; - SingleValueResponseWebhookTestResult: { - data: components["schemas"]["WebhookTestResult"]; - }; - SingleValueResponseWorkspaceDto: { - data: components["schemas"]["WorkspaceDto"]; - }; - SlackChannelConfig: Omit & { - /** @description Slack incoming webhook URL */ - webhookUrl: string; - /** @description Optional mention text included in notifications, e.g. @channel */ - mentionText?: string | null; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - channelType: "slack"; - }; - SslExpiryAssertion: Omit & { - /** - * Format: int32 - * @description Minimum days before TLS certificate expiry; fails or warns below this threshold - */ - minDaysRemaining: number; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "ssl_expiry"; - }; - StatusCodeAssertion: Omit & { - /** @description Expected status code, range pattern, or wildcard such as 2xx */ - expected: string; - /** - * @description Comparison operator (equals, contains, less_than, greater_than, etc.) - * @enum {string} - */ - operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; - } & { - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - type: "status_code"; - }; - /** @description Updated branding configuration; null preserves current */ - StatusPageBranding: { - /** @description URL for the logo image displayed in the header */ - logoUrl?: string | null; - /** @description URL for the browser tab favicon */ - faviconUrl?: string | null; - /** @description Primary brand color as hex, e.g. #4F46E5; drives accent/links/buttons */ - brandColor?: string | null; - /** @description Page body background color as hex, e.g. #FAFAFA */ - pageBackground?: string | null; - /** @description Card/surface background color as hex, e.g. #FFFFFF */ - cardBackground?: string | null; - /** @description Primary text color as hex, e.g. #09090B */ - textColor?: string | null; - /** @description Card border color as hex, e.g. #E4E4E7 */ - borderColor?: string | null; - /** @description Header layout style (reserved for future use) */ - headerStyle?: string | null; - /** @description Color theme: light or dark (default: light) */ - theme?: string | null; - /** @description URL where visitors can report a problem */ - reportUrl?: string | null; - /** - * @description Whether to hide the 'Powered by DevHelm' footer badge (default: false) - * @default false - */ - hidePoweredBy: boolean; - /** @description Custom CSS injected via