From 38d66d9bede3bfa9e5ab9ed892cf4553fae28775 Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 10 Apr 2026 21:00:52 +0200 Subject: [PATCH 1/7] Add yaml impl --- .github/workflows/spec-check.yml | 5 +- package-lock.json | 25 + package.json | 2 + src/commands/alert-channels/test.ts | 9 +- src/commands/api-keys/revoke.ts | 5 +- src/commands/auth/login.ts | 22 +- src/commands/auth/me.ts | 36 +- src/commands/data/services/status.ts | 9 +- src/commands/data/services/uptime.ts | 13 +- src/commands/dependencies/track.ts | 9 +- src/commands/deploy.ts | 137 +++ src/commands/incidents/resolve.ts | 11 +- src/commands/init.ts | 128 ++- src/commands/monitors/pause.ts | 9 +- src/commands/monitors/results.ts | 30 +- src/commands/monitors/resume.ts | 9 +- src/commands/monitors/test.ts | 9 +- src/commands/notification-policies/test.ts | 5 +- src/commands/plan.ts | 84 ++ src/commands/status.ts | 10 +- src/commands/validate.ts | 104 +- src/commands/webhooks/test.ts | 9 +- src/lib/api-client.ts | 5 +- src/lib/base-command.ts | 3 + src/lib/crud-commands.ts | 61 +- src/lib/output.ts | 1 + src/lib/resources.ts | 50 +- src/lib/typed-api.ts | 69 ++ src/lib/yaml/applier.ts | 155 +++ src/lib/yaml/differ.ts | 136 +++ src/lib/yaml/entitlements.ts | 127 +++ src/lib/yaml/handlers.ts | 780 ++++++++++++++ src/lib/yaml/index.ts | 13 + src/lib/yaml/interpolation.ts | 86 ++ src/lib/yaml/parser.ts | 159 +++ src/lib/yaml/resolver.ts | 75 ++ src/lib/yaml/schema.ts | 381 +++++++ src/lib/yaml/state.ts | 58 ++ src/lib/yaml/transform.ts | 232 +++++ src/lib/yaml/types.ts | 48 + src/lib/yaml/validator.ts | 483 +++++++++ test/fixtures/yaml/edge/all-channel-types.yml | 42 + test/fixtures/yaml/edge/all-monitor-types.yml | 48 + .../yaml/invalid/bad-channel-type.yml | 5 + test/fixtures/yaml/invalid/bad-escalation.yml | 10 + test/fixtures/yaml/invalid/bad-frequency.yml | 7 + test/fixtures/yaml/invalid/bad-type.yml | 5 + .../fixtures/yaml/invalid/duplicate-names.yml | 11 + test/fixtures/yaml/invalid/empty.yml | 2 + .../fixtures/yaml/invalid/missing-env-var.yml | 10 + test/fixtures/yaml/invalid/missing-name.yml | 5 + test/fixtures/yaml/valid/defaults.yml | 20 + test/fixtures/yaml/valid/env-vars.yml | 12 + test/fixtures/yaml/valid/full-stack.yml | 166 +++ test/fixtures/yaml/valid/minimal.yml | 6 + test/fixtures/yaml/valid/multi-a.yml | 11 + test/fixtures/yaml/valid/multi-b.yml | 11 + test/yaml/applier.test.ts | 668 ++++++++++++ test/yaml/differ.test.ts | 949 ++++++++++++++++++ test/yaml/entitlements.test.ts | 185 ++++ test/yaml/handlers.test.ts | 131 +++ test/yaml/interpolation.test.ts | 135 +++ test/yaml/parity.test.ts | 51 + test/yaml/parser.test.ts | 162 +++ test/yaml/resolver.test.ts | 94 ++ test/yaml/state.test.ts | 99 ++ test/yaml/transform.test.ts | 433 ++++++++ test/yaml/validator.test.ts | 784 +++++++++++++++ 68 files changed, 7417 insertions(+), 247 deletions(-) create mode 100644 src/commands/deploy.ts create mode 100644 src/commands/plan.ts create mode 100644 src/lib/typed-api.ts create mode 100644 src/lib/yaml/applier.ts create mode 100644 src/lib/yaml/differ.ts create mode 100644 src/lib/yaml/entitlements.ts create mode 100644 src/lib/yaml/handlers.ts create mode 100644 src/lib/yaml/index.ts create mode 100644 src/lib/yaml/interpolation.ts create mode 100644 src/lib/yaml/parser.ts create mode 100644 src/lib/yaml/resolver.ts create mode 100644 src/lib/yaml/schema.ts create mode 100644 src/lib/yaml/state.ts create mode 100644 src/lib/yaml/transform.ts create mode 100644 src/lib/yaml/types.ts create mode 100644 src/lib/yaml/validator.ts create mode 100644 test/fixtures/yaml/edge/all-channel-types.yml create mode 100644 test/fixtures/yaml/edge/all-monitor-types.yml create mode 100644 test/fixtures/yaml/invalid/bad-channel-type.yml create mode 100644 test/fixtures/yaml/invalid/bad-escalation.yml create mode 100644 test/fixtures/yaml/invalid/bad-frequency.yml create mode 100644 test/fixtures/yaml/invalid/bad-type.yml create mode 100644 test/fixtures/yaml/invalid/duplicate-names.yml create mode 100644 test/fixtures/yaml/invalid/empty.yml create mode 100644 test/fixtures/yaml/invalid/missing-env-var.yml create mode 100644 test/fixtures/yaml/invalid/missing-name.yml create mode 100644 test/fixtures/yaml/valid/defaults.yml create mode 100644 test/fixtures/yaml/valid/env-vars.yml create mode 100644 test/fixtures/yaml/valid/full-stack.yml create mode 100644 test/fixtures/yaml/valid/minimal.yml create mode 100644 test/fixtures/yaml/valid/multi-a.yml create mode 100644 test/fixtures/yaml/valid/multi-b.yml create mode 100644 test/yaml/applier.test.ts create mode 100644 test/yaml/differ.test.ts create mode 100644 test/yaml/entitlements.test.ts create mode 100644 test/yaml/handlers.test.ts create mode 100644 test/yaml/interpolation.test.ts create mode 100644 test/yaml/parity.test.ts create mode 100644 test/yaml/parser.test.ts create mode 100644 test/yaml/resolver.test.ts create mode 100644 test/yaml/state.test.ts create mode 100644 test/yaml/transform.test.ts create mode 100644 test/yaml/validator.test.ts diff --git a/.github/workflows/spec-check.yml b/.github/workflows/spec-check.yml index 88e66d1..ecbc247 100644 --- a/.github/workflows/spec-check.yml +++ b/.github/workflows/spec-check.yml @@ -31,10 +31,13 @@ jobs: - run: npm run build - run: npm test + - name: Regenerate types from spec + run: npm run typegen + - name: Check for type changes id: diff run: | - if git diff --quiet src/lib/api.generated.ts docs/openapi/monitoring-api.json; then + if git diff --quiet src/lib/api.generated.ts docs/openapi/; then echo "changed=false" >> "$GITHUB_OUTPUT" else echo "changed=true" >> "$GITHUB_OUTPUT" diff --git a/package-lock.json b/package-lock.json index ebb6eae..f021bb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@oclif/plugin-not-found": "^3.2.80", "chalk": "^5.6.2", "cli-table3": "^0.6.5", + "lodash-es": "^4.18.1", "openapi-fetch": "^0.17.0", "yaml": "^2.8.3" }, @@ -21,6 +22,7 @@ "devhelm": "bin/run.js" }, "devDependencies": { + "@types/lodash-es": "^4.17.12", "@types/node": "^25.5.2", "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.58.1", @@ -2911,6 +2913,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mute-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", @@ -5075,6 +5094,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", diff --git a/package.json b/package.json index 1225a44..68c38a6 100644 --- a/package.json +++ b/package.json @@ -63,10 +63,12 @@ "@oclif/plugin-not-found": "^3.2.80", "chalk": "^5.6.2", "cli-table3": "^0.6.5", + "lodash-es": "^4.18.1", "openapi-fetch": "^0.17.0", "yaml": "^2.8.3" }, "devDependencies": { + "@types/lodash-es": "^4.17.12", "@types/node": "^25.5.2", "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.58.1", diff --git a/src/commands/alert-channels/test.ts b/src/commands/alert-channels/test.ts index b80b171..876deef 100644 --- a/src/commands/alert-channels/test.ts +++ b/src/commands/alert-channels/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class AlertChannelsTest extends Command { static description = 'Send a test notification to an alert channel' @@ -11,10 +11,7 @@ export default class AlertChannelsTest extends Command { async run() { const {args, flags} = await this.parse(AlertChannelsTest) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/alert-channels/${args.id}/test` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (resp as any)?.data ?? resp - this.log(result.success ? 'Test notification sent successfully.' : 'Test notification failed.') + const resp = await typedPost<{data?: {success?: boolean}}>(client, `/api/v1/alert-channels/${args.id}/test`) + this.log(resp.data?.success ? 'Test notification sent successfully.' : 'Test notification failed.') } } diff --git a/src/commands/api-keys/revoke.ts b/src/commands/api-keys/revoke.ts index 1e91b82..b2ef210 100644 --- a/src/commands/api-keys/revoke.ts +++ b/src/commands/api-keys/revoke.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class ApiKeysRevoke extends Command { static description = 'Revoke an API key' @@ -11,8 +11,7 @@ export default class ApiKeysRevoke extends Command { async run() { const {args, flags} = await this.parse(ApiKeysRevoke) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await checkedFetch(client.POST(`/api/v1/api-keys/${args.id}/revoke` as any, {} as any)) + await typedPost(client, `/api/v1/api-keys/${args.id}/revoke`) this.log(`API key '${args.id}' revoked.`) } } diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index b979b6f..9d31e5c 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -1,6 +1,7 @@ import {Command, Flags} from '@oclif/core' import {globalFlags} from '../../lib/base-command.js' -import {createApiClient, checkedFetch} from '../../lib/api-client.js' +import {createApiClient} from '../../lib/api-client.js' +import {typedGet} from '../../lib/typed-api.js' import {saveContext, resolveApiUrl} from '../../lib/auth.js' import * as readline from 'node:readline' @@ -24,20 +25,18 @@ export default class AuthLogin extends Command { this.log('Validating token...') const client = createApiClient({baseUrl: apiUrl, token}) - // Try /api/v1/auth/me first (API key — returns rich identity info). - // Falls back to /api/v1/dashboard/overview for non-API-key tokens (dev tokens, JWTs). try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const me = (resp as any)?.data ?? resp + const resp = await typedGet<{data?: {organization?: {name?: string; id?: number}; key?: {name?: string}; plan?: {tier?: string}}}>( + client, '/api/v1/auth/me', + ) + 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 @@ -46,8 +45,7 @@ export default class AuthLogin extends Command { } try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any)) + await typedGet(client, '/api/v1/dashboard/overview') saveContext({name: flags.name, apiUrl, token}, true) this.log('') this.log(` Authenticated successfully.`) diff --git a/src/commands/auth/me.ts b/src/commands/auth/me.ts index 120e30d..faa65f4 100644 --- a/src/commands/auth/me.ts +++ b/src/commands/auth/me.ts @@ -1,8 +1,24 @@ import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedGet} from '../../lib/typed-api.js' import {formatOutput, OutputFormat} from '../../lib/output.js' +interface AuthMeResponse { + data?: { + key?: {id?: string; name?: string; createdAt?: string; expiresAt?: string; lastUsedAt?: string} + organization?: {id?: number; name?: string} + plan?: { + tier?: string + subscriptionStatus?: string + trialActive?: boolean + trialExpiresAt?: string + usage?: Record + entitlements?: Record + } + rateLimits?: {requestsPerMinute?: number; remaining?: number; windowMs?: number} + } +} + export default class AuthMe extends Command { static description = 'Show current API key identity, organization, plan, and rate limits' static examples = ['<%= config.bin %> auth me', '<%= config.bin %> auth me --output json'] @@ -11,10 +27,8 @@ export default class AuthMe extends Command { async run() { const {flags} = await this.parse(AuthMe) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const me = (resp as any)?.data ?? resp + const resp = await typedGet(client, '/api/v1/auth/me') + const me = resp.data const format = flags.output as OutputFormat if (format === 'json' || format === 'yaml') { @@ -22,10 +36,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') @@ -42,8 +56,8 @@ export default class AuthMe extends Command { this.log(' Rate Limits') this.log(` Limit: ${r.requestsPerMinute ?? '–'} req/min Remaining: ${r.remaining ?? '–'} Window: ${r.windowMs ? `${r.windowMs / 1000}s` : '–'}`) - const usage = p.usage as Record | undefined - const entitlements = p.entitlements as Record | undefined + const usage = p.usage + const entitlements = p.entitlements if (usage && entitlements) { this.log('') this.log(' Usage') diff --git a/src/commands/data/services/status.ts b/src/commands/data/services/status.ts index 0df5429..acd5d23 100644 --- a/src/commands/data/services/status.ts +++ b/src/commands/data/services/status.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' -import {checkedFetch} from '../../../lib/api-client.js' +import {typedGet} from '../../../lib/typed-api.js' export default class DataServicesStatus extends Command { static description = 'Get the current status of a service' @@ -11,10 +11,7 @@ export default class DataServicesStatus extends Command { async run() { const {args, flags} = await this.parse(DataServicesStatus) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET(`/api/v1/services/${args.slug}` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const service = (resp as any)?.data ?? resp - display(this, service, flags.output) + const resp = await typedGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}`) + display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/data/services/uptime.ts b/src/commands/data/services/uptime.ts index 1236dd7..1b6410b 100644 --- a/src/commands/data/services/uptime.ts +++ b/src/commands/data/services/uptime.ts @@ -1,6 +1,6 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' -import {checkedFetch} from '../../../lib/api-client.js' +import {typedGet} from '../../../lib/typed-api.js' export default class DataServicesUptime extends Command { static description = 'Get uptime data for a service' @@ -18,12 +18,9 @@ export default class DataServicesUptime extends Command { async run() { const {args, flags} = await this.parse(DataServicesUptime) const client = buildClient(flags) - let path = `/api/v1/services/${args.slug}/uptime?period=${flags.period}` - if (flags.granularity) path += `&granularity=${flags.granularity}` - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET(path as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const uptime = (resp as any)?.data ?? resp - display(this, uptime, flags.output) + const query: Record = {period: flags.period} + if (flags.granularity) query.granularity = flags.granularity + const resp = await typedGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}/uptime`, query) + display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/dependencies/track.ts b/src/commands/dependencies/track.ts index 0887f34..d92033f 100644 --- a/src/commands/dependencies/track.ts +++ b/src/commands/dependencies/track.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class DependenciesTrack extends Command { static description = 'Start tracking a service as a dependency' @@ -11,10 +11,7 @@ export default class DependenciesTrack extends Command { async run() { const {args, flags} = await this.parse(DependenciesTrack) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/service-subscriptions/${args.slug}` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sub = (resp as any)?.data ?? resp - this.log(`Now tracking '${sub.serviceName}' as a dependency.`) + const resp = await typedPost<{data?: {serviceName?: string}}>(client, `/api/v1/service-subscriptions/${args.slug}`) + this.log(`Now tracking '${resp.data?.serviceName}' as a dependency.`) } } diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts new file mode 100644 index 0000000..1f7f1b7 --- /dev/null +++ b/src/commands/deploy.ts @@ -0,0 +1,137 @@ +import {Command, Flags} from '@oclif/core' +import {createApiClient} from '../lib/api-client.js' +import {resolveToken, resolveApiUrl} from '../lib/auth.js' +import {loadConfig, validate, fetchAllRefs, diff, formatPlan, apply, writeState, buildState} from '../lib/yaml/index.js' +import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' + +export default class Deploy extends Command { + static description = 'Deploy devhelm.yml configuration to the DevHelm API' + + static examples = [ + '<%= config.bin %> deploy', + '<%= config.bin %> deploy --yes', + '<%= config.bin %> deploy -f monitors.yml', + '<%= config.bin %> deploy --prune --yes', + '<%= config.bin %> deploy --dry-run', + ] + + static flags = { + file: Flags.string({ + char: 'f', + description: 'Config file or directory (can be specified multiple times)', + multiple: true, + default: ['devhelm.yml'], + }), + yes: Flags.boolean({ + char: 'y', + description: 'Skip confirmation prompt (for CI)', + default: false, + }), + prune: Flags.boolean({ + description: 'Delete CLI-managed resources not present in config', + default: false, + }), + 'dry-run': Flags.boolean({ + description: 'Show what would change without applying (same as "devhelm plan")', + default: false, + }), + 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-token': Flags.string({description: 'Override API token'}), + verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), + } + + async run() { + const {flags} = await this.parse(Deploy) + + let config + try { + config = loadConfig(flags.file) + } catch (err) { + this.error((err as Error).message, {exit: 1}) + } + + const result = validate(config) + if (result.errors.length > 0) { + this.log(`\nValidation failed: ${result.errors.length} error(s)\n`) + for (const e of result.errors) { + this.log(` ✗ ${e.path}: ${e.message}`) + } + this.error('Fix validation errors before deploying', {exit: 4}) + } + + const token = flags['api-token'] ?? resolveToken() + if (!token) { + this.error('No API token configured. Run "devhelm auth login" or set DEVHELM_API_TOKEN.', {exit: 1}) + } + + const client = createApiClient({ + baseUrl: flags['api-url'] ?? resolveApiUrl(), + token, + verbose: flags.verbose, + }) + + this.log('Fetching current state from API...') + const refs = await fetchAllRefs(client) + + const changeset = diff(config, refs, {prune: flags.prune}) + + const entitlementCheck = await checkEntitlements(client, changeset) + if (entitlementCheck) { + this.log(entitlementCheck.header) + } + + const plan = formatPlan(changeset) + this.log(`\n${plan}\n`) + + if (entitlementCheck && entitlementCheck.warnings.length > 0) { + this.log(formatEntitlementWarnings(entitlementCheck.warnings)) + this.log('') + } + + const totalChanges = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length + if (totalChanges === 0) { + return + } + + if (flags['dry-run']) { + this.log('Dry run — no changes applied.') + this.exit(2) + } + + if (!flags.yes) { + const {createInterface} = await import('node:readline') + const rl = createInterface({input: process.stdin, output: process.stdout}) + const answer = await new Promise((resolve) => { + rl.question('Apply these changes? (yes/no): ', resolve) + }) + rl.close() + if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') { + this.log('Cancelled.') + return + } + } + + this.log('Applying changes...') + const applyResult = await apply(changeset, refs, client) + + for (const s of applyResult.succeeded) { + const icon = s.action === 'delete' ? '-' : s.action === 'update' ? '~' : '+' + this.log(` ${icon} ${s.resourceType} "${s.refKey}" — ${s.action}d`) + } + + if (applyResult.failed.length > 0) { + this.log('') + for (const f of applyResult.failed) { + this.log(` ✗ ${f.resourceType} "${f.refKey}" — ${f.action} failed: ${f.error}`) + } + } + + writeState(buildState(applyResult.stateEntries)) + + this.log(`\nDone: ${applyResult.succeeded.length} succeeded, ${applyResult.failed.length} failed.`) + + if (applyResult.failed.length > 0) { + this.exit(2) + } + } +} diff --git a/src/commands/incidents/resolve.ts b/src/commands/incidents/resolve.ts index 93b7360..f815c4d 100644 --- a/src/commands/incidents/resolve.ts +++ b/src/commands/incidents/resolve.ts @@ -1,6 +1,6 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class IncidentsResolve extends Command { static description = 'Resolve an incident' @@ -15,12 +15,7 @@ export default class IncidentsResolve extends Command { const {args, flags} = await this.parse(IncidentsResolve) const client = buildClient(flags) const body = flags.message ? {message: flags.message} : undefined - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const opts = body ? {body: body as any} : {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/incidents/${args.id}/resolve` as any, opts as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const incident = (resp as any)?.data ?? resp - this.log(`Incident '${incident.title}' resolved.`) + const resp = await typedPost<{data?: {title?: string}}>(client, `/api/v1/incidents/${args.id}/resolve`, body) + this.log(`Incident '${resp.data?.title}' resolved.`) } } diff --git a/src/commands/init.ts b/src/commands/init.ts index bc03cc9..2cbf94f 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -1,43 +1,112 @@ import {Command, Flags} from '@oclif/core' import {existsSync, writeFileSync} from 'node:fs' -const TEMPLATE = `# devhelm.yml — DevHelm monitor configuration +const TEMPLATE = `# devhelm.yml — DevHelm monitoring-as-code configuration # Docs: https://docs.devhelm.io/cli/configuration +# Run "devhelm validate" to check, "devhelm deploy" to apply. +version: "1" + +# defaults: +# monitors: +# frequency: 60 +# regions: [us-east, eu-west] +# enabled: true + +tags: + - name: production + color: "#EF4444" + +# environments: +# - name: Production +# slug: production +# isDefault: true + +# secrets: +# - key: bearer-token +# value: \${API_TOKEN} + +alertChannels: + - name: ops-slack + type: slack + config: + webhookUrl: \${SLACK_WEBHOOK_URL:-https://hooks.slack.com/services/REPLACE_ME} + +# notificationPolicies: +# - name: critical-escalation +# enabled: true +# priority: 1 +# escalation: +# steps: +# - channels: [ops-slack] +# delayMinutes: 0 + +# webhooks: +# - url: https://hooks.example.com/devhelm +# events: [monitor.down, monitor.recovered] + +# resourceGroups: +# - name: API Services +# monitors: [API Health Check] monitors: - name: Website Health Check type: HTTP - url: https://example.com - interval: 60 - regions: - - us-east-1 - - eu-west-1 + config: + url: https://example.com + method: GET + verifyTls: true + frequency: 60 + regions: [us-east, eu-west] + tags: [production] + alertChannels: [ops-slack] assertions: - - type: STATUS_CODE - operator: EQUALS - value: "200" - - type: RESPONSE_TIME - operator: LESS_THAN - value: "2000" - alertChannels: - - default-slack - - # - name: API Endpoint + - type: StatusCodeAssertion + config: + expected: "200" + operator: equals + severity: fail + - type: ResponseTimeAssertion + config: + thresholdMs: 2000 + severity: warn + - type: SslExpiryAssertion + config: + minDaysRemaining: 30 + severity: warn + + # - name: API Health Check # type: HTTP - # url: https://api.example.com/health - # method: GET - # interval: 30 - # timeout: 10000 + # config: + # url: https://api.example.com/health + # method: GET + # frequency: 30 # - name: DNS Check # type: DNS - # url: example.com - # interval: 300 + # config: + # hostname: example.com + # recordTypes: [A, AAAA, MX] + # frequency: 300 + # assertions: + # - type: DnsResolvesAssertion + # severity: fail - # - name: Heartbeat + # - name: Heartbeat Worker # type: HEARTBEAT - # interval: 120 - # grace: 300 + # config: + # expectedInterval: 120 + # gracePeriod: 300 + + # - name: MCP Assistant + # type: MCP_SERVER + # config: + # command: npx + # args: ["-y", "@company/mcp-server"] + # frequency: 300 + +# dependencies: +# - service: github +# alertSensitivity: INCIDENTS_ONLY ` export default class Init extends Command { @@ -60,8 +129,13 @@ export default class Init extends Command { this.error(`${flags.path} already exists. Use --force to overwrite.`, {exit: 1}) } - writeFileSync(flags.path, TEMPLATE) + try { + writeFileSync(flags.path, TEMPLATE) + } catch (err) { + this.error(`Failed to write ${flags.path}: ${err instanceof Error ? err.message : String(err)}`, {exit: 1}) + } this.log(`Created ${flags.path}`) - this.log('Edit the file, then run `devhelm validate` to check it.') + this.log('Edit the file, then run "devhelm validate" to check it.') + this.log('When ready, run "devhelm deploy" to apply it to your DevHelm account.') } } diff --git a/src/commands/monitors/pause.ts b/src/commands/monitors/pause.ts index f0cb71f..650bbcc 100644 --- a/src/commands/monitors/pause.ts +++ b/src/commands/monitors/pause.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class MonitorsPause extends Command { static description = 'Pause a monitor' @@ -11,10 +11,7 @@ export default class MonitorsPause extends Command { async run() { const {args, flags} = await this.parse(MonitorsPause) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/monitors/${args.id}/pause` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const monitor = (resp as any)?.data ?? resp - this.log(`Monitor '${monitor.name}' paused.`) + const resp = await typedPost<{data?: {name?: string}}>(client, `/api/v1/monitors/${args.id}/pause`) + this.log(`Monitor '${resp.data?.name}' paused.`) } } diff --git a/src/commands/monitors/results.ts b/src/commands/monitors/results.ts index b7e4c47..9d1f07e 100644 --- a/src/commands/monitors/results.ts +++ b/src/commands/monitors/results.ts @@ -1,6 +1,15 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedGet} from '../../lib/typed-api.js' + +interface MonitorResult { + id?: string + status?: string + responseTime?: number + statusCode?: number + region?: string + checkedAt?: string +} export default class MonitorsResults extends Command { static description = 'Show recent check results for a monitor' @@ -14,17 +23,14 @@ export default class MonitorsResults extends Command { async run() { const {args, flags} = await this.parse(MonitorsResults) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET(`/api/v1/monitors/${args.id}/results?limit=${flags.limit}` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items = (resp as any)?.data ?? resp - display(this, items, flags.output, [ - {header: 'ID', get: (r: Record) => String(r.id ?? '')}, - {header: 'STATUS', get: (r: Record) => String(r.status ?? '')}, - {header: 'RESPONSE TIME', get: (r: Record) => String(r.responseTime ?? '')}, - {header: 'CODE', get: (r: Record) => String(r.statusCode ?? '')}, - {header: 'REGION', get: (r: Record) => String(r.region ?? '')}, - {header: 'CHECKED AT', get: (r: Record) => String(r.checkedAt ?? '')}, + const resp = await typedGet<{data?: MonitorResult[]}>(client, `/api/v1/monitors/${args.id}/results`, {limit: flags.limit}) + display(this, resp.data ?? [], flags.output, [ + {header: 'ID', get: (r) => String(r.id ?? '')}, + {header: 'STATUS', get: (r) => String(r.status ?? '')}, + {header: 'RESPONSE TIME', get: (r) => String(r.responseTime ?? '')}, + {header: 'CODE', get: (r) => String(r.statusCode ?? '')}, + {header: 'REGION', get: (r) => String(r.region ?? '')}, + {header: 'CHECKED AT', get: (r) => String(r.checkedAt ?? '')}, ]) } } diff --git a/src/commands/monitors/resume.ts b/src/commands/monitors/resume.ts index 29d0636..2cca88d 100644 --- a/src/commands/monitors/resume.ts +++ b/src/commands/monitors/resume.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class MonitorsResume extends Command { static description = 'Resume a paused monitor' @@ -11,10 +11,7 @@ export default class MonitorsResume extends Command { async run() { const {args, flags} = await this.parse(MonitorsResume) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/monitors/${args.id}/resume` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const monitor = (resp as any)?.data ?? resp - this.log(`Monitor '${monitor.name}' resumed.`) + const resp = await typedPost<{data?: {name?: string}}>(client, `/api/v1/monitors/${args.id}/resume`) + this.log(`Monitor '${resp.data?.name}' resumed.`) } } diff --git a/src/commands/monitors/test.ts b/src/commands/monitors/test.ts index c7ff541..f58ac8a 100644 --- a/src/commands/monitors/test.ts +++ b/src/commands/monitors/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class MonitorsTest extends Command { static description = 'Run an ad-hoc test for a monitor' @@ -12,10 +12,7 @@ export default class MonitorsTest extends Command { const {args, flags} = await this.parse(MonitorsTest) const client = buildClient(flags) this.log('Running test...') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/monitors/${args.id}/test` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (resp as any)?.data ?? resp - display(this, result, flags.output) + const resp = await typedPost<{data?: Record}>(client, `/api/v1/monitors/${args.id}/test`) + display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/notification-policies/test.ts b/src/commands/notification-policies/test.ts index 110f507..3eaf186 100644 --- a/src/commands/notification-policies/test.ts +++ b/src/commands/notification-policies/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class NotificationPoliciesTest extends Command { static description = 'Test a notification policy' @@ -11,8 +11,7 @@ export default class NotificationPoliciesTest extends Command { async run() { const {args, flags} = await this.parse(NotificationPoliciesTest) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await checkedFetch(client.POST(`/api/v1/notification-policies/${args.id}/test` as any, {} as any)) + await typedPost(client, `/api/v1/notification-policies/${args.id}/test`) this.log('Test dispatch sent.') } } diff --git a/src/commands/plan.ts b/src/commands/plan.ts new file mode 100644 index 0000000..a471070 --- /dev/null +++ b/src/commands/plan.ts @@ -0,0 +1,84 @@ +import {Command, Flags} from '@oclif/core' +import {createApiClient} from '../lib/api-client.js' +import {resolveToken, resolveApiUrl} from '../lib/auth.js' +import {loadConfig, validate, fetchAllRefs, diff, formatPlan} from '../lib/yaml/index.js' +import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' + +export default class Plan extends Command { + static description = 'Show what "devhelm deploy" would change without applying' + + static examples = [ + '<%= config.bin %> plan', + '<%= config.bin %> plan -f monitors.yml', + '<%= config.bin %> plan --prune', + ] + + static flags = { + file: Flags.string({ + char: 'f', + description: 'Config file or directory (can be specified multiple times)', + multiple: true, + default: ['devhelm.yml'], + }), + prune: Flags.boolean({ + description: 'Include deletions of CLI-managed resources not in config', + default: false, + }), + 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-token': Flags.string({description: 'Override API token'}), + verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), + } + + async run() { + const {flags} = await this.parse(Plan) + + let config + try { + config = loadConfig(flags.file) + } catch (err) { + this.error((err as Error).message, {exit: 1}) + } + + const result = validate(config) + if (result.errors.length > 0) { + this.log(`\nValidation failed: ${result.errors.length} error(s)\n`) + for (const e of result.errors) { + this.log(` ✗ ${e.path}: ${e.message}`) + } + this.error('Fix validation errors first', {exit: 4}) + } + + const token = flags['api-token'] ?? resolveToken() + if (!token) { + this.error('No API token configured. Run "devhelm auth login" or set DEVHELM_API_TOKEN.', {exit: 1}) + } + + const client = createApiClient({ + baseUrl: flags['api-url'] ?? resolveApiUrl(), + token, + verbose: flags.verbose, + }) + + this.log('Fetching current state from API...') + const refs = await fetchAllRefs(client) + + const changeset = diff(config, refs, {prune: flags.prune}) + + const entitlementCheck = await checkEntitlements(client, changeset) + if (entitlementCheck) { + this.log(entitlementCheck.header) + } + + this.log(`\n${formatPlan(changeset)}`) + + if (entitlementCheck && entitlementCheck.warnings.length > 0) { + this.log('') + this.log(formatEntitlementWarnings(entitlementCheck.warnings)) + } + + const total = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length + if (total > 0) { + this.exit(2) + } + } +} diff --git a/src/commands/status.ts b/src/commands/status.ts index 36150be..dfa79cb 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -1,6 +1,6 @@ import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../lib/base-command.js' -import {checkedFetch} from '../lib/api-client.js' +import {typedGet} from '../lib/typed-api.js' import {formatOutput, OutputFormat} from '../lib/output.js' export default class Status extends Command { @@ -11,10 +11,8 @@ export default class Status extends Command { async run() { const {flags} = await this.parse(Status) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const overview = (resp as any)?.data ?? resp + const resp = await typedGet<{data?: Record>}>(client, '/api/v1/dashboard/overview') + const overview = resp.data ?? {} const format = flags.output as OutputFormat if (format === 'json' || format === 'yaml') { @@ -32,7 +30,7 @@ export default class Status extends Command { 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(i.mttr30d / 60)}m` : '–'}`) + 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('') } } diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 777ff06..9b46d7e 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -1,85 +1,79 @@ -import {Command, Args} from '@oclif/core' -import {existsSync, readFileSync} from 'node:fs' -import {parse as parseYaml} from 'yaml' - -interface MonitorConfig { - name?: string - type?: string - url?: string - interval?: number -} - -interface DevhelmConfig { - monitors?: MonitorConfig[] -} - -const VALID_TYPES = new Set(['HTTP', 'DNS', 'TCP', 'ICMP', 'HEARTBEAT']) +import {Command, Args, Flags} from '@oclif/core' +import {parseConfigFile, validate} from '../lib/yaml/index.js' export default class Validate extends Command { - static description = 'Validate a devhelm.yml configuration file' + static description = 'Validate a devhelm.yml configuration file against the full schema' static examples = [ '<%= config.bin %> validate', '<%= config.bin %> validate devhelm.yml', + '<%= config.bin %> validate --strict', ] static args = { file: Args.string({description: 'Config file path', default: 'devhelm.yml'}), } - async run() { - const {args} = await this.parse(Validate) - - if (!existsSync(args.file)) { - this.error(`File not found: ${args.file}`, {exit: 1}) - } + static flags = { + strict: Flags.boolean({ + description: 'Fail on warnings (unresolved cross-references, etc.)', + default: false, + }), + 'skip-env': Flags.boolean({ + description: 'Skip environment variable interpolation (syntax check only)', + default: false, + }), + } - const raw = readFileSync(args.file, 'utf8') - let config: DevhelmConfig + async run() { + const {args, flags} = await this.parse(Validate) + let config try { - config = parseYaml(raw) as DevhelmConfig + config = parseConfigFile(args.file, !flags['skip-env']) } catch (err) { - this.error(`Invalid YAML: ${(err as Error).message}`, {exit: 4}) + this.error((err as Error).message, {exit: 1}) } - const errors: string[] = [] - - if (!config.monitors || !Array.isArray(config.monitors)) { - errors.push('Missing or invalid "monitors" array') - } else { - for (let i = 0; i < config.monitors.length; i++) { - const m = config.monitors[i] - const prefix = `monitors[${i}]` - - if (!m.name) errors.push(`${prefix}: "name" is required`) - if (!m.type) { - errors.push(`${prefix}: "type" is required`) - } else if (!VALID_TYPES.has(m.type.toUpperCase())) { - errors.push(`${prefix}: invalid type "${m.type}" (must be one of: ${[...VALID_TYPES].join(', ')})`) - } + const result = validate(config) - if (m.type && m.type.toUpperCase() !== 'HEARTBEAT' && !m.url) { - errors.push(`${prefix}: "url" is required for ${m.type} monitors`) - } - - if (m.interval !== undefined && (typeof m.interval !== 'number' || m.interval < 10)) { - errors.push(`${prefix}: "interval" must be a number >= 10`) - } + if (result.warnings.length > 0 && !flags.strict) { + this.log(`\n${args.file}: ${result.warnings.length} warning(s)\n`) + for (const w of result.warnings) { + this.log(` ⚠ ${w.path}: ${w.message}`) } + this.log('') } - if (errors.length > 0) { - this.log(`\n${args.file}: ${errors.length} error(s)\n`) - for (const e of errors) { - this.log(` ✗ ${e}`) + if (result.errors.length > 0) { + this.log(`\n${args.file}: ${result.errors.length} error(s)\n`) + for (const e of result.errors) { + this.log(` ✗ ${e.path}: ${e.message}`) } + this.log('') + this.exit(4) + } + if (flags.strict && result.warnings.length > 0) { + this.log(`\n${args.file}: ${result.warnings.length} warning(s) (strict mode)\n`) + for (const w of result.warnings) { + this.log(` ✗ ${w.path}: ${w.message}`) + } this.log('') this.exit(4) } - const count = config.monitors?.length ?? 0 - this.log(`${args.file}: valid (${count} monitor${count !== 1 ? 's' : ''})`) + const sections: string[] = [] + if (config.monitors?.length) sections.push(`${config.monitors.length} monitor(s)`) + if (config.alertChannels?.length) sections.push(`${config.alertChannels.length} alert channel(s)`) + if (config.tags?.length) sections.push(`${config.tags.length} tag(s)`) + if (config.environments?.length) sections.push(`${config.environments.length} environment(s)`) + if (config.secrets?.length) sections.push(`${config.secrets.length} secret(s)`) + if (config.notificationPolicies?.length) sections.push(`${config.notificationPolicies.length} notification policy(ies)`) + if (config.webhooks?.length) sections.push(`${config.webhooks.length} webhook(s)`) + if (config.resourceGroups?.length) sections.push(`${config.resourceGroups.length} resource group(s)`) + if (config.dependencies?.length) sections.push(`${config.dependencies.length} dependency(ies)`) + + this.log(`${args.file}: valid (${sections.join(', ')})`) } } diff --git a/src/commands/webhooks/test.ts b/src/commands/webhooks/test.ts index b2ef883..af03969 100644 --- a/src/commands/webhooks/test.ts +++ b/src/commands/webhooks/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {checkedFetch} from '../../lib/api-client.js' +import {typedPost} from '../../lib/typed-api.js' export default class WebhooksTest extends Command { static description = 'Send a test event to a webhook' @@ -11,10 +11,7 @@ export default class WebhooksTest extends Command { async run() { const {args, flags} = await this.parse(WebhooksTest) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(`/api/v1/webhooks/${args.id}/test` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = (resp as any)?.data ?? resp - this.log(result.success ? 'Test event delivered.' : 'Test delivery failed.') + const resp = await typedPost<{data?: {success?: boolean}}>(client, `/api/v1/webhooks/${args.id}/test`) + this.log(resp.data?.success ? 'Test event delivered.' : 'Test delivery failed.') } } diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 432572a..6adc150 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -77,11 +77,10 @@ export type ApiClient = ReturnType * Unwrap an openapi-fetch response: returns `data` on success, throws `ApiRequestError` on failure. * Every client.GET / POST / PUT / DELETE call should be wrapped with this. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export async function checkedFetch(promise: Promise<{data?: T; error?: any; response: Response}>): Promise { +export async function checkedFetch(promise: Promise<{data?: T; error?: unknown; response: Response}>): Promise { const {data, error, response} = await promise if (error || !response.ok) { - const body = typeof error === 'object' ? JSON.stringify(error) : String(error ?? 'Unknown error') + const body = typeof error === 'object' && error !== null ? JSON.stringify(error) : String(error ?? 'Unknown error') throw new ApiRequestError(response.status, response.statusText, body) } return data as T diff --git a/src/lib/base-command.ts b/src/lib/base-command.ts index 3d850b5..ca9fa6a 100644 --- a/src/lib/base-command.ts +++ b/src/lib/base-command.ts @@ -29,11 +29,14 @@ export function buildClient(flags: { return createApiClient({baseUrl, token, verbose: flags.verbose}) } +const VALID_FORMATS = new Set(['table', 'json', 'yaml']) + export function display( command: Command, data: unknown, format: string, columns?: ColumnDef[], ): void { + if (!VALID_FORMATS.has(format)) format = 'table' command.log(formatOutput(data, format as OutputFormat, columns)) } diff --git a/src/lib/crud-commands.ts b/src/lib/crud-commands.ts index 8ff48d9..bff3cf5 100644 --- a/src/lib/crud-commands.ts +++ b/src/lib/crud-commands.ts @@ -1,23 +1,29 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from './base-command.js' -import {checkedFetch} from './api-client.js' -import {ColumnDef} from './output.js' +import {typedGet, typedPost, typedPut, typedDelete} from './typed-api.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 AnyFlag = any +type OclifFlag = any -export interface ResourceConfig { +export interface ResourceConfig { name: string plural: string apiPath: string idField?: string columns: ColumnDef[] - createFlags?: Record - updateFlags?: Record - bodyBuilder?: (flags: Record) => Record + createFlags?: Record + updateFlags?: Record + bodyBuilder?: (flags: Record) => object } -export function createListCommand(config: ResourceConfig) { +interface PaginatedResponse { + data?: T[] + hasNext?: boolean +} + +export function createListCommand(config: ResourceConfig) { class ListCmd extends Command { static description = `List all ${config.plural}` static examples = [`<%= config.bin %> ${config.plural} list`] @@ -26,18 +32,15 @@ export function createListCommand(config: ResourceConfig) { async run() { const {flags} = await this.parse(ListCmd) const client = buildClient(flags) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET(config.apiPath as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const items = (resp as any)?.data ?? resp - display(this, items, flags.output, config.columns) + const resp = await typedGet>(client, config.apiPath) + display(this, resp.data ?? [], flags.output, config.columns) } } return ListCmd } -export function createGetCommand(config: ResourceConfig) { +export function createGetCommand(config: ResourceConfig) { const idLabel = config.idField ?? 'id' class GetCmd extends Command { static description = `Get a ${config.name} by ${idLabel}` @@ -49,18 +52,15 @@ export function createGetCommand(config: ResourceConfig) { const {args, flags} = await this.parse(GetCmd) const client = buildClient(flags) const id = args[idLabel] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.GET(`${config.apiPath}/${id}` as any, {} as any)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const item = (resp as any)?.data ?? resp - display(this, item, flags.output) + const resp = await typedGet<{data?: T}>(client, `${config.apiPath}/${id}`) + display(this, resp.data ?? resp, flags.output) } } return GetCmd } -export function createCreateCommand(config: ResourceConfig) { +export function createCreateCommand(config: ResourceConfig) { const resourceFlags = config.createFlags ?? {} class CreateCmd extends Command { static description = `Create a new ${config.name}` @@ -72,18 +72,15 @@ export function createCreateCommand(config: ResourceConfig) { const client = buildClient(flags) const raw = extractResourceFlags(flags, Object.keys(resourceFlags)) const body = config.bodyBuilder ? config.bodyBuilder(raw) : raw - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.POST(config.apiPath as any, {body: body as any})) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const item = (resp as any)?.data ?? resp - display(this, item, flags.output) + const resp = await typedPost<{data?: T}>(client, config.apiPath, body) + display(this, resp.data ?? resp, flags.output) } } return CreateCmd } -export function createUpdateCommand(config: ResourceConfig) { +export function createUpdateCommand(config: ResourceConfig) { const idLabel = config.idField ?? 'id' const resourceFlags = config.updateFlags ?? config.createFlags ?? {} class UpdateCmd extends Command { @@ -98,18 +95,15 @@ export function createUpdateCommand(config: ResourceConfig) { const id = args[idLabel] const raw = extractResourceFlags(flags, Object.keys(resourceFlags)) const body = config.bodyBuilder ? config.bodyBuilder(raw) : raw - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resp = await checkedFetch(client.PUT(`${config.apiPath}/${id}` as any, {body: body as any})) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const item = (resp as any)?.data ?? resp - display(this, item, flags.output) + const resp = await typedPut<{data?: T}>(client, `${config.apiPath}/${id}`, body) + display(this, resp.data ?? resp, flags.output) } } return UpdateCmd } -export function createDeleteCommand(config: ResourceConfig) { +export function createDeleteCommand(config: ResourceConfig) { const idLabel = config.idField ?? 'id' class DeleteCmd extends Command { static description = `Delete a ${config.name}` @@ -121,8 +115,7 @@ export function createDeleteCommand(config: ResourceConfig) { const {args, flags} = await this.parse(DeleteCmd) const client = buildClient(flags) const id = args[idLabel] - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await checkedFetch(client.DELETE(`${config.apiPath}/${id}` as any, {} as any)) + await typedDelete(client, `${config.apiPath}/${id}`) this.log(`${config.name} '${id}' deleted.`) } } diff --git a/src/lib/output.ts b/src/lib/output.ts index 55dddb6..6c0fee9 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -3,6 +3,7 @@ import {stringify as yamlStringify} from 'yaml' export type OutputFormat = 'table' | 'json' | 'yaml' +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- contravariant T makes unknown impractical here export interface ColumnDef { header: string get: (row: T) => string diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 9334c55..42234a4 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -86,7 +86,7 @@ export const MONITORS: ResourceConfig = { method: Flags.string({description: desc('HttpMonitorConfig', 'method'), options: HTTP_METHODS}), port: Flags.string({description: desc('TcpMonitorConfig', 'port', 'TCP port to connect to')}), }, - bodyBuilder: (raw): CreateMonitorRequest | Record => { + bodyBuilder: (raw) => { const monitorType = raw.type as MonitorType | undefined if (monitorType) { const body: CreateMonitorRequest = { @@ -99,7 +99,7 @@ export const MONITORS: ResourceConfig = { if (raw.regions) { body.regions = String(raw.regions).split(',').map((s) => s.trim()).filter(Boolean) } - return body as unknown as Record + return body } const body: Record = {} if (raw.name !== undefined) body.name = raw.name @@ -111,22 +111,28 @@ export const MONITORS: ResourceConfig = { }, } +/** + * Generated config types extend `Record` (OAS generator artifact for + * abstract base class MonitorConfig), which prevents direct object literal assignment. + * The single cast at the end is the narrowest workaround. + */ function buildMonitorConfig(type: MonitorType, raw: Record): CreateMonitorRequest['config'] { + const method: HttpMethod = (raw.method as HttpMethod) || 'GET' switch (type) { case 'HTTP': - return {url: String(raw.url ?? ''), method: (raw.method as HttpMethod) || 'GET'} as unknown as Schemas['HttpMonitorConfig'] + return {url: String(raw.url ?? ''), method} as CreateMonitorRequest['config'] case 'TCP': - return {host: String(raw.url ?? ''), port: raw.port ? Number(raw.port) : 443} as unknown as Schemas['TcpMonitorConfig'] + return {host: String(raw.url ?? ''), port: raw.port ? Number(raw.port) : 443} as CreateMonitorRequest['config'] case 'DNS': - return {hostname: String(raw.url ?? '')} as unknown as Schemas['DnsMonitorConfig'] + return {hostname: String(raw.url ?? '')} as CreateMonitorRequest['config'] case 'ICMP': - return {host: String(raw.url ?? '')} as unknown as Schemas['IcmpMonitorConfig'] + return {host: String(raw.url ?? '')} as CreateMonitorRequest['config'] case 'HEARTBEAT': - return {expectedInterval: 60, gracePeriod: 60} as unknown as Schemas['HeartbeatMonitorConfig'] + return {expectedInterval: 60, gracePeriod: 60} as CreateMonitorRequest['config'] case 'MCP_SERVER': - return {command: String(raw.url ?? '')} as unknown as Schemas['McpServerMonitorConfig'] + return {command: String(raw.url ?? '')} as CreateMonitorRequest['config'] default: - return {url: String(raw.url ?? ''), method: 'GET'} as unknown as Schemas['HttpMonitorConfig'] + return {url: String(raw.url ?? ''), method: 'GET'} as CreateMonitorRequest['config'] } } @@ -152,14 +158,14 @@ export const INCIDENTS: ResourceConfig = { 'monitor-id': Flags.string({description: desc('CreateManualIncidentRequest', 'monitorId')}), body: Flags.string({description: desc('CreateManualIncidentRequest', 'body')}), }, - bodyBuilder: (raw): Record => { + bodyBuilder: (raw) => { const body: CreateManualIncidentRequest = { title: String(raw.title), severity: raw.severity as IncidentSeverity, } if (raw['monitor-id'] !== undefined) body.monitorId = String(raw['monitor-id']) if (raw.body !== undefined) body.body = String(raw.body) - return body as unknown as Record + return body }, } @@ -190,7 +196,7 @@ export const ALERT_CHANNELS: ResourceConfig = { config: Flags.string({description: 'Channel-specific configuration as JSON'}), 'webhook-url': Flags.string({description: desc('SlackChannelConfig', 'webhookUrl', 'Slack/Discord/Teams webhook URL')}), }, - bodyBuilder: (raw): Record => { + bodyBuilder: (raw) => { let config: CreateAlertChannelRequest['config'] | undefined if (raw.config) { config = JSON.parse(String(raw.config)) as CreateAlertChannelRequest['config'] @@ -203,8 +209,8 @@ export const ALERT_CHANNELS: ResourceConfig = { config = {channelType} as CreateAlertChannelRequest['config'] } } - const body: Record = {} - if (raw.name !== undefined) body.name = raw.name + const body: Partial = {} + if (raw.name !== undefined) body.name = String(raw.name) if (config !== undefined) body.config = config return body }, @@ -230,7 +236,7 @@ export const NOTIFICATION_POLICIES: ResourceConfig = { 'channel-ids': Flags.string({description: desc('EscalationStep', 'channelIds', 'Comma-separated alert channel IDs')}), enabled: Flags.boolean({description: desc('UpdateNotificationPolicyRequest', 'enabled'), allowNo: true}), }, - bodyBuilder: (raw): Record => { + bodyBuilder: (raw) => { const channelIds = raw['channel-ids'] ? String(raw['channel-ids']).split(',').map((s) => s.trim()).filter(Boolean) : [] @@ -240,7 +246,7 @@ export const NOTIFICATION_POLICIES: ResourceConfig = { enabled: (raw.enabled as boolean) ?? true, priority: 0, } - return body as unknown as Record + return body }, } @@ -345,13 +351,13 @@ export const WEBHOOKS: ResourceConfig = { events: Flags.string({description: desc('UpdateWebhookEndpointRequest', 'subscribedEvents')}), description: Flags.string({description: desc('UpdateWebhookEndpointRequest', 'description')}), }, - bodyBuilder: (raw): Record => { - const body: Record = {} - if (raw.url !== undefined) body.url = raw.url + bodyBuilder: (raw) => { + const body: Partial = {} + if (raw.url !== undefined) body.url = String(raw.url) if (raw.events !== undefined) { body.subscribedEvents = String(raw.events).split(',').map((s) => s.trim()).filter(Boolean) } - if (raw.description !== undefined) body.description = raw.description + if (raw.description !== undefined) body.description = String(raw.description) return body }, } @@ -371,10 +377,10 @@ export const API_KEYS: ResourceConfig = { name: Flags.string({description: desc('CreateApiKeyRequest', 'name'), required: true}), 'expires-at': Flags.string({description: desc('CreateApiKeyRequest', 'expiresAt')}), }, - bodyBuilder: (raw): Record => { + bodyBuilder: (raw) => { const body: CreateApiKeyRequest = {name: String(raw.name)} if (raw['expires-at'] !== undefined) body.expiresAt = String(raw['expires-at']) - return body as unknown as Record + return body }, } diff --git a/src/lib/typed-api.ts b/src/lib/typed-api.ts new file mode 100644 index 0000000..1b0266d --- /dev/null +++ b/src/lib/typed-api.ts @@ -0,0 +1,69 @@ +/** + * Typed API transport layer. + * + * The OpenAPI spec includes a required `actor` query parameter on every + * operation — a Spring Security @AuthenticationPrincipal artefact. The + * server resolves the actor from the auth token; clients never send it. + * This forces `as any` casts when calling openapi-fetch methods. + * + * This module isolates those casts to ONE file while exposing fully-typed + * generic responses. All handler / resolver / applier code specifies the + * expected DTO type via the generic, achieving compile-time safety + * everywhere else. + * + * TODO(api): Add @Hidden to the @AuthenticationPrincipal parameter in API + * controllers, regenerate the OpenAPI spec, then remove these casts and + * call client.GET/POST/PUT/DELETE directly with full path-level types. + */ +import type {ApiClient} from './api-client.js' +import {checkedFetch} from './api-client.js' + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export function typedGet(client: ApiClient, path: string, query?: Record): Promise { + return checkedFetch(client.GET(path as any, query ? {params: {query} as any} : (undefined as any))) +} + +export function typedPost(client: ApiClient, path: string, body?: unknown): Promise { + return checkedFetch(client.POST(path as any, body !== undefined ? ({body} as any) : (undefined as any))) +} + +export function typedPut(client: ApiClient, path: string, body?: unknown): Promise { + return checkedFetch(client.PUT(path as any, body !== undefined ? ({body} as any) : (undefined as any))) +} + +export function typedPatch(client: ApiClient, path: string, body?: unknown): Promise { + return checkedFetch(client.PATCH(path as any, body !== undefined ? ({body} as any) : (undefined as any))) +} + +export function typedDelete(client: ApiClient, path: string): Promise { + return checkedFetch(client.DELETE(path as any, undefined as any)) +} + +/* eslint-enable @typescript-eslint/no-explicit-any */ + +// ── Pagination helper ─────────────────────────────────────────────────── + +interface PaginatedResponse { + data?: T[] + hasNext?: boolean +} + +const API_PAGE_SIZE = 200 + +export async function fetchPaginated( + client: ApiClient, + path: string, +): Promise { + const results: TItem[] = [] + let page = 0 + + while (true) { + const resp = await typedGet>(client, path, {page, size: API_PAGE_SIZE}) + results.push(...(resp.data ?? [])) + if (!resp.hasNext) break + page++ + } + + return results +} diff --git a/src/lib/yaml/applier.ts b/src/lib/yaml/applier.ts new file mode 100644 index 0000000..b15d07b --- /dev/null +++ b/src/lib/yaml/applier.ts @@ -0,0 +1,155 @@ +/** + * Executes a changeset against the API in dependency order. + * + * Delegates all per-resource-type create/update/delete operations to typed + * handlers in handlers.ts — no switch/case or `as YamlFoo` casts here. + */ +import type {ApiClient} from '../api-client.js' +import {typedPost, typedDelete} from '../typed-api.js' +import {HANDLER_MAP} from './handlers.js' +import type {Changeset, Change, HandledResourceType} from './types.js' +import type {ResolvedRefs} from './resolver.js' +import type {StateEntry} from './state.js' + +export interface ApplyResult { + succeeded: AppliedChange[] + failed: FailedChange[] + stateEntries: StateEntry[] +} + +export interface AppliedChange { + action: string + resourceType: string + refKey: string + id?: string +} + +export interface FailedChange { + action: string + resourceType: string + refKey: string + error: string +} + +/** + * Apply the changeset to the API. Returns results with successes/failures. + * Updates refs in-place as new resources are created (for downstream refs). + */ +export async function apply( + changeset: Changeset, + refs: ResolvedRefs, + client: ApiClient, +): Promise { + const succeeded: AppliedChange[] = [] + const failed: FailedChange[] = [] + const stateEntries: StateEntry[] = [] + + for (const change of changeset.creates) { + try { + const handler = lookupHandler(change.resourceType, 'create') + const id = await handler.applyCreate(change.desired, refs, client) + if (id) { + refs.set(handler.refType, change.refKey, { + id, refKey: change.refKey, raw: change.desired as Record, + }) + stateEntries.push({ + resourceType: change.resourceType, + refKey: change.refKey, + id, + createdAt: new Date().toISOString(), + }) + succeeded.push({action: 'create', resourceType: change.resourceType, refKey: change.refKey, id}) + } else { + failed.push({ + action: 'create', resourceType: change.resourceType, + refKey: change.refKey, error: 'Create succeeded but API returned no resource ID', + }) + } + } catch (err) { + failed.push({ + action: 'create', resourceType: change.resourceType, + refKey: change.refKey, error: errorMessage(err), + }) + } + } + + for (const change of changeset.updates) { + try { + const handler = lookupHandler(change.resourceType, 'update') + await handler.applyUpdate(change.desired, change.existingId!, refs, client) + succeeded.push({action: 'update', resourceType: change.resourceType, refKey: change.refKey, id: change.existingId}) + if (change.existingId) { + stateEntries.push({ + resourceType: change.resourceType, + refKey: change.refKey, + id: change.existingId, + createdAt: new Date().toISOString(), + }) + } + } catch (err) { + failed.push({ + action: 'update', resourceType: change.resourceType, + refKey: change.refKey, error: errorMessage(err), + }) + } + } + + for (const change of changeset.deletes) { + try { + const handler = lookupHandler(change.resourceType, 'delete') + const path = handler.deletePath(change.existingId!) + await typedDelete(client, path) + succeeded.push({action: 'delete', resourceType: change.resourceType, refKey: change.refKey}) + } catch (err) { + failed.push({ + action: 'delete', resourceType: change.resourceType, + refKey: change.refKey, error: errorMessage(err), + }) + } + } + + for (const change of changeset.memberships) { + try { + await applyMembership(change, refs, client) + succeeded.push({action: 'membership', resourceType: 'groupMembership', refKey: change.refKey}) + } catch (err) { + failed.push({ + action: 'membership', resourceType: 'groupMembership', + refKey: change.refKey, error: errorMessage(err), + }) + } + } + + return {succeeded, failed, stateEntries} +} + +interface MembershipPayload { + groupName: string + memberType: string + memberRef: string +} + +async function applyMembership(change: Change, refs: ResolvedRefs, client: ApiClient): Promise { + const desired = change.desired as MembershipPayload + const groupId = refs.require('resourceGroups', desired.groupName) + const memberType = desired.memberType + + let memberId: string + if (memberType === 'monitor') { + memberId = refs.require('monitors', desired.memberRef) + } else { + memberId = refs.require('dependencies', desired.memberRef) + } + + await typedPost(client, `/api/v1/resource-groups/${groupId}/members`, {memberType, memberId}) +} + +function lookupHandler(resourceType: string, action: string) { + const handler = HANDLER_MAP[resourceType as HandledResourceType] + if (!handler) throw new Error(`Unknown resource type for ${action}: ${resourceType}`) + return handler +} + +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err) +} diff --git a/src/lib/yaml/differ.ts b/src/lib/yaml/differ.ts new file mode 100644 index 0000000..182b12d --- /dev/null +++ b/src/lib/yaml/differ.ts @@ -0,0 +1,136 @@ +/** + * Diff engine: compares desired state (YAML) against current state (API) + * and produces an ordered changeset. + * + * Delegates all per-resource-type semantic comparison to typed handlers + * in handlers.ts — no Record anywhere in this file. + */ +import type {DevhelmConfig} from './schema.js' +import type {ResolvedRefs} from './resolver.js' +import {allHandlers, type ResourceHandler} from './handlers.js' +import type {Change, Changeset, DiffOptions} from './types.js' +import {RESOURCE_ORDER} from './types.js' + +// Re-export types so existing consumers don't need to change imports +export type {ChangeAction, ResourceType, Change, DiffOptions, Changeset} from './types.js' + +// ── Main diff function ───────────────────────────────────────────────── + +export function diff(config: DevhelmConfig, refs: ResolvedRefs, options: DiffOptions = {}): Changeset { + const creates: Change[] = [] + const updates: Change[] = [] + const deletes: Change[] = [] + const memberships: Change[] = [] + + for (const handler of allHandlers()) { + diffSection(handler, config[handler.configKey], refs, creates, updates, deletes, options) + } + + for (const group of config.resourceGroups ?? []) { + for (const monitorName of group.monitors ?? []) { + memberships.push({ + action: 'create', + resourceType: 'groupMembership', + refKey: `${group.name} → ${monitorName}`, + desired: {groupName: group.name, memberType: 'monitor', memberRef: monitorName}, + }) + } + for (const serviceSlug of group.services ?? []) { + memberships.push({ + action: 'create', + resourceType: 'groupMembership', + refKey: `${group.name} → ${serviceSlug}`, + desired: {groupName: group.name, memberType: 'service', memberRef: serviceSlug}, + }) + } + } + + creates.sort((a, b) => RESOURCE_ORDER.indexOf(a.resourceType) - RESOURCE_ORDER.indexOf(b.resourceType)) + updates.sort((a, b) => RESOURCE_ORDER.indexOf(a.resourceType) - RESOURCE_ORDER.indexOf(b.resourceType)) + deletes.sort((a, b) => RESOURCE_ORDER.indexOf(b.resourceType) - RESOURCE_ORDER.indexOf(a.resourceType)) + + return {creates, updates, deletes, memberships} +} + +// ── Generic diff section ──────────────────────────────────────────────── + +function diffSection( + handler: ResourceHandler, + items: unknown[] | undefined, + refs: ResolvedRefs, + creates: Change[], + updates: Change[], + deletes: Change[], + options: DiffOptions, +): void { + const desired = new Set() + + for (const item of items ?? []) { + const refKey = handler.getRefKey(item) + desired.add(refKey) + const existing = refs.get(handler.refType, refKey) + + if (existing) { + if (handler.hasChanged(item, existing.raw, refs)) { + updates.push({ + action: 'update', + resourceType: handler.resourceType, + refKey, + existingId: existing.id, + desired: item, + current: existing.raw, + }) + } + } else { + creates.push({ + action: 'create', + resourceType: handler.resourceType, + refKey, + desired: item, + }) + } + } + + if (options.prune && items !== undefined) { + for (const entry of refs.allEntries(handler.refType)) { + if (!desired.has(entry.refKey)) { + if (handler.resourceType === 'monitor' && entry.managedBy !== 'CLI') continue + deletes.push({ + action: 'delete', + resourceType: handler.resourceType, + refKey: entry.refKey, + existingId: entry.id, + current: entry.raw, + }) + } + } + } +} + +// ── Plan formatting ──────────────────────────────────────────────────── + +export function formatPlan(changeset: Changeset): string { + const lines: string[] = [] + const totalChanges = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length + + if (totalChanges === 0) { + return 'No changes. Infrastructure is up-to-date.' + } + + lines.push(`Plan: ${changeset.creates.length} to create, ${changeset.updates.length} to update, ${changeset.deletes.length} to delete, ${changeset.memberships.length} memberships\n`) + + for (const c of changeset.creates) { + lines.push(` + ${c.resourceType} "${c.refKey}"`) + } + for (const c of changeset.updates) { + lines.push(` ~ ${c.resourceType} "${c.refKey}"`) + } + for (const c of changeset.deletes) { + lines.push(` - ${c.resourceType} "${c.refKey}"`) + } + for (const c of changeset.memberships) { + lines.push(` → ${c.refKey}`) + } + + return lines.join('\n') +} diff --git a/src/lib/yaml/entitlements.ts b/src/lib/yaml/entitlements.ts new file mode 100644 index 0000000..df78e96 --- /dev/null +++ b/src/lib/yaml/entitlements.ts @@ -0,0 +1,127 @@ +/** + * Pre-flight entitlement check: fetches /auth/me and compares + * planned resource creation against plan limits. + */ +import type {ApiClient} from '../api-client.js' +import {typedGet} from '../typed-api.js' +import type {Changeset} from './types.js' + +interface Entitlement { + value: number +} + +interface AuthMePlan { + tier?: string + entitlements?: Record + usage?: Record + trialActive?: boolean + subscriptionStatus?: string +} + +interface AuthMeData { + plan?: AuthMePlan + organization?: {name?: string} +} + +export interface EntitlementWarning { + resource: string + current: number + creating: number + limit: number +} + +export interface EntitlementCheck { + plan: string + warnings: EntitlementWarning[] + header: string +} + +const UNLIMITED = Number.MAX_SAFE_INTEGER + +const RESOURCE_ENTITLEMENT_MAP: Record = { + monitor: 'monitors', + alertChannel: 'alert_channels', + notificationPolicy: 'notification_policies', + webhook: 'webhooks', + resourceGroup: 'resource_groups', + environment: 'environments', + secret: 'secrets', +} + +/** + * Fetch plan entitlements and check if the changeset would exceed any limits. + * Returns null if /auth/me is unavailable (non-API-key tokens). + */ +export async function checkEntitlements( + client: ApiClient, + changeset: Changeset, +): Promise { + let data: AuthMeData + try { + const resp = await typedGet(client, '/api/v1/auth/me') + data = narrowAuthMeData(resp) + } catch { + return null + } + + const plan = data.plan + if (!plan?.entitlements || !plan.usage) return null + + const createCounts = new Map() + for (const create of changeset.creates) { + const entitlementKey = RESOURCE_ENTITLEMENT_MAP[create.resourceType] + if (entitlementKey) { + createCounts.set(entitlementKey, (createCounts.get(entitlementKey) ?? 0) + 1) + } + } + + const warnings: EntitlementWarning[] = [] + + for (const [entitlementKey, createsOfType] of createCounts) { + const entitlement = plan.entitlements[entitlementKey] + if (!entitlement || entitlement.value >= UNLIMITED) continue + + const currentUsage = plan.usage[entitlementKey] ?? 0 + if (currentUsage + createsOfType > entitlement.value) { + warnings.push({ + resource: entitlementKey, + current: currentUsage, + creating: createsOfType, + limit: entitlement.value, + }) + } + } + + const tier = plan.tier ?? 'unknown' + const org = data.organization?.name ?? '' + const usageParts: string[] = [] + for (const [key, used] of Object.entries(plan.usage)) { + const limit = plan.entitlements[key]?.value + if (limit != null && limit < UNLIMITED) { + usageParts.push(`${key.replace(/_/g, ' ')}: ${used}/${limit}`) + } + } + + const header = `Plan: ${tier}${org ? ` (${org})` : ''}${usageParts.length ? ' | ' + usageParts.join(', ') : ''}` + + return {plan: tier, warnings, header} +} + +function narrowAuthMeData(resp: unknown): AuthMeData { + if (!resp || typeof resp !== 'object') return {} + const obj = resp as Record + const inner = (obj.data && typeof obj.data === 'object' ? obj.data : resp) as Record + return { + plan: inner.plan && typeof inner.plan === 'object' ? inner.plan as AuthMePlan : undefined, + organization: inner.organization && typeof inner.organization === 'object' + ? inner.organization as AuthMeData['organization'] + : undefined, + } +} + +export function formatEntitlementWarnings(warnings: EntitlementWarning[]): string { + const lines = warnings.map((w) => + ` ⚠ ${w.resource}: deploying ${w.creating} new but only ${w.limit - w.current} remaining (${w.current}/${w.limit} used)`, + ) + return lines.join('\n') +} diff --git a/src/lib/yaml/handlers.ts b/src/lib/yaml/handlers.ts new file mode 100644 index 0000000..c06eeb5 --- /dev/null +++ b/src/lib/yaml/handlers.ts @@ -0,0 +1,780 @@ +/** + * Typed resource handlers — the single source of truth for each resource type's + * identity, semantic comparison, API operations, and list fetching. + * + * Every handler is defined with FULL TypeScript generics over its YAML input type + * (what the user writes in devhelm.yml), its API DTO type (what the API returns), + * and a Snapshot type used for drift detection. + * + * Drift detection uses "snapshot comparison": + * - toDesiredSnapshot(yaml, api, refs) → TSnapshot (what we WANT) + * - toCurrentSnapshot(api) → TSnapshot (what we HAVE) + * - hasChanged = !isEqual(desired, current) + * + * TypeScript enforces both functions return the same TSnapshot type. Adding a + * field to TSnapshot → compile error until both sides are updated. + * + * Adding a new resource type requires: + * 1. Adding it to HandledResourceType in types.ts + * 2. Implementing a handler here (with snapshot functions) + * 3. Adding it to HANDLER_MAP (TypeScript errors if you forget) + */ +import {createHash} from 'node:crypto' +import isEqual from 'lodash-es/isEqual.js' +import type {components} from '../api.generated.js' +import type {ApiClient} from '../api-client.js' +import type {ResolvedRefs} from './resolver.js' +import type {HandledResourceType, RefType} from './types.js' +import type { + YamlTag, YamlEnvironment, YamlSecret, YamlAlertChannel, + YamlNotificationPolicy, YamlWebhook, YamlResourceGroup, + YamlMonitor, YamlDependency, +} from './schema.js' +import type {YamlSectionKey} from './schema.js' +import { + toCreateTagRequest, toCreateEnvironmentRequest, toCreateSecretRequest, + toCreateAlertChannelRequest, toCreateNotificationPolicyRequest, + toCreateWebhookRequest, toCreateResourceGroupRequest, + toCreateMonitorRequest, toUpdateMonitorRequest, +} from './transform.js' +import {typedPost, typedPut, typedPatch, fetchPaginated} from '../typed-api.js' + +type Schemas = components['schemas'] + +// ── Response wrappers (match generated SingleValueResponse* pattern) ───── + +interface SingleValueResponse { + data?: T +} + +// ── Public interface ──────────────────────────────────────────────────── + +/** + * Generic handler for a YAML-managed resource type. + * + * TYaml = the type the user writes in devhelm.yml (e.g. YamlTag) + * TApiDto = the DTO the API returns (e.g. TagDto) + * + * The registry stores ResourceHandler (defaults → unknown) for heterogeneous + * storage. defineHandler verifies all field accesses at compile time, + * then type-erases to the default form. + */ +export interface ResourceHandler { + readonly resourceType: HandledResourceType + readonly refType: RefType + readonly configKey: YamlSectionKey + readonly listPath: string + + getRefKey(yaml: TYaml): string + getApiRefKey(api: TApiDto): string + getApiId(api: TApiDto): string + getManagedBy?(api: TApiDto): string | undefined + + hasChanged(yaml: TYaml, api: TApiDto, refs: ResolvedRefs): boolean + + fetchAll(client: ApiClient): Promise + applyCreate(yaml: TYaml, refs: ResolvedRefs, client: ApiClient): Promise + applyUpdate(yaml: TYaml, existingId: string, refs: ResolvedRefs, client: ApiClient): Promise + deletePath(id: string): string +} + +// ── Handler definition (snapshot-based) ───────────────────────────────── + +/** + * Input shape for defineHandler. Handlers provide two snapshot functions + * that both return TSnapshot, OR set alwaysChanged for write-only resources. + * hasChanged is automatically derived — handlers never implement it manually. + */ +interface HandlerDef { + readonly resourceType: HandledResourceType + readonly refType: RefType + readonly configKey: YamlSectionKey + readonly listPath: string + + getRefKey(yaml: TYaml): string + getApiRefKey(api: TApiDto): string + getApiId(api: TApiDto): string + getManagedBy?: (api: TApiDto) => string | undefined + + /** + * true → resource always reports as changed (e.g. secrets: value is write-only, + * the API never returns it, so we can't compare). + */ + alwaysChanged?: boolean + + /** + * Project the YAML config + current API state into a comparable snapshot. + * For undefined (optional) YAML fields, use the current API value so they + * don't trigger a false diff. + */ + toDesiredSnapshot?: (yaml: TYaml, api: TApiDto, refs: ResolvedRefs) => TSnapshot + + /** + * Project the API DTO into the same comparable snapshot shape. + */ + toCurrentSnapshot?: (api: TApiDto) => TSnapshot + + fetchAll(client: ApiClient): Promise + applyCreate(yaml: TYaml, refs: ResolvedRefs, client: ApiClient): Promise + applyUpdate(yaml: TYaml, existingId: string, refs: ResolvedRefs, client: ApiClient): Promise + deletePath(id: string): string +} + +/** + * Type-checking bridge: takes a handler definition with full generic types, + * derives hasChanged from snapshot comparison, then type-erases to + * ResourceHandler (defaults) for registry storage. + */ +function defineHandler( + h: HandlerDef, +): ResourceHandler { + const handler: ResourceHandler = { + resourceType: h.resourceType, + refType: h.refType, + configKey: h.configKey, + listPath: h.listPath, + + getRefKey: h.getRefKey, + getApiRefKey: h.getApiRefKey, + getApiId: h.getApiId, + getManagedBy: h.getManagedBy, + + hasChanged(yaml: TYaml, api: TApiDto, refs: ResolvedRefs): boolean { + if (h.alwaysChanged) return true + if (!h.toDesiredSnapshot || !h.toCurrentSnapshot) return true + return !isEqual(h.toDesiredSnapshot(yaml, api, refs), h.toCurrentSnapshot(api)) + }, + + fetchAll: h.fetchAll, + applyCreate: h.applyCreate, + applyUpdate: h.applyUpdate, + deletePath: h.deletePath, + } + return handler as unknown as ResourceHandler +} + +// ── Shared helpers ────────────────────────────────────────────────────── + +function nonNullStrings(arr: (string | null)[] | null | undefined): string[] { + return (arr ?? []).filter((v): v is string => v !== null) +} + +function sortedIds(ids: string[]): string[] { + return [...ids].sort() +} + +function sha256Hex(input: string): string { + return createHash('sha256').update(input, 'utf8').digest('hex') +} + +/** + * Deterministic JSON serialization with alphabetically sorted keys at every + * nesting level. Produces the same output regardless of JS engine key + * insertion order, matching the Java-side TreeMap-based canonical JSON. + */ +function stableStringify(obj: unknown): string { + if (obj === null || obj === undefined) return 'null' + if (typeof obj !== 'object') return JSON.stringify(obj) + if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']' + const record = obj as Record + const keys = Object.keys(record).sort() + return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(record[k])).join(',') + '}' +} + +// ── Tag ───────────────────────────────────────────────────────────────── + +interface TagSnapshot { + name: string + color: string | null +} + +const tagHandler = defineHandler({ + resourceType: 'tag', + refType: 'tags', + configKey: 'tags', + listPath: '/api/v1/tags', + + getRefKey: (yaml) => yaml.name, + getApiRefKey: (api) => api.name ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml, api) => ({ + name: yaml.name, + color: yaml.color ?? api.color ?? null, + }), + toCurrentSnapshot: (api) => ({ + name: api.name ?? '', + color: api.color ?? null, + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/tags'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, '/api/v1/tags', toCreateTagRequest(yaml), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, _refs, client) { + await typedPut(client, `/api/v1/tags/${id}`, toCreateTagRequest(yaml)) + }, + deletePath: (id) => `/api/v1/tags/${id}`, +}) + +// ── Environment ───────────────────────────────────────────────────────── + +interface EnvironmentSnapshot { + name: string + isDefault: boolean + variables: Record | null +} + +const environmentHandler = defineHandler({ + resourceType: 'environment', + refType: 'environments', + configKey: 'environments', + listPath: '/api/v1/environments', + + getRefKey: (yaml) => yaml.slug, + getApiRefKey: (api) => api.slug ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml, api) => ({ + name: yaml.name, + isDefault: yaml.isDefault ?? api.isDefault ?? false, + variables: yaml.variables ?? api.variables ?? null, + }), + toCurrentSnapshot: (api) => ({ + name: api.name ?? '', + isDefault: api.isDefault ?? false, + variables: api.variables ?? null, + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/environments'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, '/api/v1/environments', toCreateEnvironmentRequest(yaml), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, _refs, client) { + await typedPut(client, `/api/v1/environments/${id}`, { + name: yaml.name, variables: yaml.variables ?? null, isDefault: yaml.isDefault, + }) + }, + deletePath: (id) => `/api/v1/environments/${id}`, +}) + +// ── Secret ────────────────────────────────────────────────────────────── + +interface SecretSnapshot { + key: string + valueHash: string +} + +const secretHandler = defineHandler({ + resourceType: 'secret', + refType: 'secrets', + configKey: 'secrets', + listPath: '/api/v1/secrets', + + getRefKey: (yaml) => yaml.key, + getApiRefKey: (api) => api.key ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml) => ({ + key: yaml.key, + valueHash: sha256Hex(yaml.value), + }), + toCurrentSnapshot: (api) => ({ + key: api.key ?? '', + valueHash: api.valueHash ?? '', + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/secrets'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, '/api/v1/secrets', toCreateSecretRequest(yaml), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, _id, _refs, client) { + await typedPut(client, `/api/v1/secrets/${yaml.key}`, {value: yaml.value}) + }, + deletePath: (id) => `/api/v1/secrets/${id}`, +}) + +// ── Alert Channel ─────────────────────────────────────────────────────── + +interface AlertChannelSnapshot { + name: string + channelType: string + configHash: string +} + +const alertChannelHandler = defineHandler({ + resourceType: 'alertChannel', + refType: 'alertChannels', + configKey: 'alertChannels', + listPath: '/api/v1/alert-channels', + + getRefKey: (yaml) => yaml.name, + getApiRefKey: (api) => api.name, + getApiId: (api) => api.id, + + toDesiredSnapshot: (yaml) => { + const req = toCreateAlertChannelRequest(yaml) + return { + name: req.name, + channelType: yaml.type, + configHash: sha256Hex(stableStringify(req.config)), + } + }, + toCurrentSnapshot: (api) => ({ + name: api.name, + channelType: api.channelType?.toLowerCase?.() ?? '', + // configHash is available once the API is deployed with V89 migration. + // Pre-migration responses lack it → empty string → forces update (backfills hash). + configHash: (api as Record).configHash as string ?? '', + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/alert-channels'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, '/api/v1/alert-channels', toCreateAlertChannelRequest(yaml), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, _refs, client) { + await typedPut(client, `/api/v1/alert-channels/${id}`, toCreateAlertChannelRequest(yaml)) + }, + deletePath: (id) => `/api/v1/alert-channels/${id}`, +}) + +// ── Notification Policy ───────────────────────────────────────────────── + +interface NotificationPolicySnapshot { + name: string + enabled: boolean + priority: number + matchRules: unknown + escalation: unknown +} + +const notificationPolicyHandler = defineHandler({ + resourceType: 'notificationPolicy', + refType: 'notificationPolicies', + configKey: 'notificationPolicies', + listPath: '/api/v1/notification-policies', + + getRefKey: (yaml) => yaml.name, + getApiRefKey: (api) => api.name ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml, api, refs) => { + const req = toCreateNotificationPolicyRequest(yaml, refs) + return { + name: req.name, + enabled: req.enabled ?? api.enabled ?? true, + priority: req.priority ?? api.priority ?? 0, + matchRules: req.matchRules ?? api.matchRules ?? null, + escalation: req.escalation, + } + }, + toCurrentSnapshot: (api) => ({ + name: api.name ?? '', + enabled: api.enabled ?? true, + priority: api.priority ?? 0, + matchRules: api.matchRules ?? null, + escalation: api.escalation ?? null, + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/notification-policies'), + + async applyCreate(yaml, refs, client) { + const resp = await typedPost>( + client, '/api/v1/notification-policies', toCreateNotificationPolicyRequest(yaml, refs), + ) + return resp.data?.id != null ? String(resp.data.id) : undefined + }, + async applyUpdate(yaml, id, refs, client) { + await typedPut(client, `/api/v1/notification-policies/${id}`, toCreateNotificationPolicyRequest(yaml, refs)) + }, + deletePath: (id) => `/api/v1/notification-policies/${id}`, +}) + +// ── Webhook ───────────────────────────────────────────────────────────── + +interface WebhookSnapshot { + url: string + description: string | null + subscribedEvents: string[] +} + +const webhookHandler = defineHandler({ + resourceType: 'webhook', + refType: 'webhooks', + configKey: 'webhooks', + listPath: '/api/v1/webhooks', + + getRefKey: (yaml) => yaml.url, + getApiRefKey: (api) => api.url ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml, api) => ({ + url: yaml.url, + description: yaml.description ?? api.description ?? null, + subscribedEvents: sortedIds(yaml.events), + }), + toCurrentSnapshot: (api) => ({ + url: api.url ?? '', + description: api.description ?? null, + subscribedEvents: sortedIds(api.subscribedEvents ?? []), + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/webhooks'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, '/api/v1/webhooks', toCreateWebhookRequest(yaml), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, _refs, client) { + await typedPut(client, `/api/v1/webhooks/${id}`, toCreateWebhookRequest(yaml)) + }, + deletePath: (id) => `/api/v1/webhooks/${id}`, +}) + +// ── Resource Group ────────────────────────────────────────────────────── + +interface ResourceGroupSnapshot { + name: string + description: string | null + alertPolicyId: string | null + defaultFrequency: number | null + defaultRegions: string[] | null + defaultRetryStrategy: unknown + defaultAlertChannelIds: string[] | null + defaultEnvironmentId: string | null + healthThresholdType: string | null + healthThresholdValue: number | null + suppressMemberAlerts: boolean | undefined + confirmationDelaySeconds: number | null + recoveryCooldownMinutes: number | null +} + +const resourceGroupHandler = defineHandler({ + resourceType: 'resourceGroup', + refType: 'resourceGroups', + configKey: 'resourceGroups', + listPath: '/api/v1/resource-groups', + + getRefKey: (yaml) => yaml.name, + getApiRefKey: (api) => api.name ?? '', + getApiId: (api) => String(api.id ?? ''), + + toDesiredSnapshot: (yaml, api, refs) => ({ + name: yaml.name, + description: yaml.description ?? api.description ?? null, + alertPolicyId: yaml.alertPolicy !== undefined + ? (refs.resolve('notificationPolicies', yaml.alertPolicy) ?? null) + : (api.alertPolicyId ?? null), + defaultFrequency: yaml.defaultFrequency ?? api.defaultFrequency ?? null, + defaultRegions: yaml.defaultRegions !== undefined + ? sortedIds(yaml.defaultRegions) + : (api.defaultRegions ? sortedIds(nonNullStrings(api.defaultRegions)) : null), + defaultRetryStrategy: yaml.defaultRetryStrategy ?? api.defaultRetryStrategy ?? null, + defaultAlertChannelIds: yaml.defaultAlertChannels !== undefined + ? sortedIds(yaml.defaultAlertChannels.map((n) => refs.resolve('alertChannels', n) ?? n)) + : (api.defaultAlertChannels ? sortedIds(nonNullStrings(api.defaultAlertChannels)) : null), + defaultEnvironmentId: yaml.defaultEnvironment !== undefined + ? (refs.resolve('environments', yaml.defaultEnvironment) ?? null) + : (api.defaultEnvironmentId ?? null), + healthThresholdType: yaml.healthThresholdType ?? api.healthThresholdType ?? null, + healthThresholdValue: yaml.healthThresholdValue ?? api.healthThresholdValue ?? null, + suppressMemberAlerts: yaml.suppressMemberAlerts ?? api.suppressMemberAlerts, + confirmationDelaySeconds: yaml.confirmationDelaySeconds ?? api.confirmationDelaySeconds ?? null, + recoveryCooldownMinutes: yaml.recoveryCooldownMinutes ?? api.recoveryCooldownMinutes ?? null, + }), + toCurrentSnapshot: (api) => ({ + name: api.name ?? '', + description: api.description ?? null, + alertPolicyId: api.alertPolicyId ?? null, + defaultFrequency: api.defaultFrequency ?? null, + defaultRegions: api.defaultRegions ? sortedIds(nonNullStrings(api.defaultRegions)) : null, + defaultRetryStrategy: api.defaultRetryStrategy ?? null, + defaultAlertChannelIds: api.defaultAlertChannels ? sortedIds(nonNullStrings(api.defaultAlertChannels)) : null, + defaultEnvironmentId: api.defaultEnvironmentId ?? null, + healthThresholdType: api.healthThresholdType ?? null, + healthThresholdValue: api.healthThresholdValue ?? null, + suppressMemberAlerts: api.suppressMemberAlerts, + confirmationDelaySeconds: api.confirmationDelaySeconds ?? null, + recoveryCooldownMinutes: api.recoveryCooldownMinutes ?? null, + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/resource-groups'), + + async applyCreate(yaml, refs, client) { + const resp = await typedPost>( + client, '/api/v1/resource-groups', toCreateResourceGroupRequest(yaml, refs), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, refs, client) { + await typedPut(client, `/api/v1/resource-groups/${id}`, toCreateResourceGroupRequest(yaml, refs)) + }, + deletePath: (id) => `/api/v1/resource-groups/${id}`, +}) + +// ── Monitor ───────────────────────────────────────────────────────────── + +interface MonitorSnapshot { + name: string + type: string + config: unknown + enabled: boolean | undefined + frequencySeconds: number | undefined + regions: string[] | null + environmentId: string | null + tagIds: string[] | null + alertChannelIds: string[] | null + auth: unknown + assertions: unknown + incidentPolicy: unknown +} + +const monitorHandler = defineHandler({ + resourceType: 'monitor', + refType: 'monitors', + configKey: 'monitors', + listPath: '/api/v1/monitors', + + getRefKey: (yaml) => yaml.name, + getApiRefKey: (api) => api.name ?? '', + getApiId: (api) => String(api.id ?? ''), + getManagedBy: (api) => api.managedBy, + + toDesiredSnapshot: (yaml, api, refs) => ({ + name: yaml.name, + type: yaml.type, + config: yaml.config, + enabled: yaml.enabled ?? api.enabled, + frequencySeconds: yaml.frequency ?? api.frequencySeconds, + regions: yaml.regions !== undefined + ? sortedIds(yaml.regions) + : (api.regions ? sortedIds(api.regions) : null), + environmentId: yaml.environment !== undefined + ? (refs.resolve('environments', yaml.environment) ?? null) + : (api.environment?.id ?? null), + tagIds: yaml.tags !== undefined + ? sortedIds(yaml.tags.map((n) => refs.resolve('tags', n) ?? n)) + : extractTagIds(api), + alertChannelIds: yaml.alertChannels !== undefined + ? sortedIds(yaml.alertChannels.map((n) => refs.resolve('alertChannels', n) ?? n)) + : (api.alertChannelIds ? sortedIds(nonNullStrings(api.alertChannelIds)) : null), + auth: yaml.auth !== undefined + ? normalizeYamlAuth(yaml.auth, refs) + : normalizeApiAuth(api.auth), + assertions: yaml.assertions !== undefined + ? normalizeYamlAssertions(yaml.assertions) + : normalizeApiAssertions(api.assertions), + incidentPolicy: yaml.incidentPolicy !== undefined + ? normalizeIncidentPolicy(yaml.incidentPolicy) + : normalizeApiIncidentPolicy(api.incidentPolicy), + }), + toCurrentSnapshot: (api) => ({ + name: api.name ?? '', + type: api.type ?? '', + config: api.config, + enabled: api.enabled, + frequencySeconds: api.frequencySeconds, + regions: api.regions ? sortedIds(api.regions) : null, + environmentId: api.environment?.id ?? null, + tagIds: extractTagIds(api), + alertChannelIds: api.alertChannelIds ? sortedIds(nonNullStrings(api.alertChannelIds)) : null, + auth: normalizeApiAuth(api.auth), + assertions: normalizeApiAssertions(api.assertions), + incidentPolicy: normalizeApiIncidentPolicy(api.incidentPolicy), + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/monitors'), + + async applyCreate(yaml, refs, client) { + const resp = await typedPost>( + client, '/api/v1/monitors', toCreateMonitorRequest(yaml, refs), + ) + return resp.data?.id ?? undefined + }, + async applyUpdate(yaml, id, refs, client) { + await typedPut(client, `/api/v1/monitors/${id}`, toUpdateMonitorRequest(yaml, refs)) + }, + deletePath: (id) => `/api/v1/monitors/${id}`, +}) + +// ── Monitor snapshot normalization helpers ─────────────────────────────── + +function extractTagIds(api: Schemas['MonitorDto']): string[] | null { + if (!api.tags) return null + return sortedIds(api.tags.map((t) => String(t.id ?? '')).filter(Boolean)) +} + +interface NormalizedAuth { + type: string + secretId: string | null + headerName?: string +} + +const AUTH_TYPE_CANONICAL: Record = { + BearerAuthConfig: 'bearer', + BasicAuthConfig: 'basic', + ApiKeyAuthConfig: 'api_key', + HeaderAuthConfig: 'header', + bearer: 'bearer', + basic: 'basic', + api_key: 'api_key', + header: 'header', +} + +function normalizeYamlAuth(auth: YamlMonitor['auth'], refs: ResolvedRefs): NormalizedAuth | null { + if (!auth) return null + const base: NormalizedAuth = { + type: AUTH_TYPE_CANONICAL[auth.type] ?? auth.type, + secretId: refs.resolve('secrets', auth.secret) ?? null, + } + if ('headerName' in auth) base.headerName = auth.headerName + return base +} + +function normalizeApiAuth(auth: Schemas['MonitorDto']['auth']): NormalizedAuth | null { + if (!auth) return null + const config = auth.config as Record | undefined + const base: NormalizedAuth = { + type: AUTH_TYPE_CANONICAL[auth.authType ?? ''] ?? (auth.authType ?? ''), + secretId: (config?.vaultSecretId as string | null) ?? null, + } + if (config?.headerName) base.headerName = config.headerName as string + return base +} + +interface NormalizedAssertion { + type: string + config: Record + severity: string +} + +function normalizeYamlAssertions(assertions: YamlMonitor['assertions']): NormalizedAssertion[] | null { + if (!assertions) return null + return assertions + .map((a) => ({type: a.type, config: a.config ?? {}, severity: a.severity ?? 'fail'})) + .sort((a, b) => a.type.localeCompare(b.type)) +} + +function normalizeApiAssertions(assertions: Schemas['MonitorDto']['assertions']): NormalizedAssertion[] | null { + if (!assertions) return null + return assertions + .map((a) => { + const config = (a.config ?? {}) as Record + const {type, ...rest} = config + return {type: type as string, config: rest, severity: a.severity ?? 'fail'} + }) + .sort((a, b) => a.type.localeCompare(b.type)) +} + +function normalizeIncidentPolicy(policy: YamlMonitor['incidentPolicy']): unknown { + if (!policy) return null + return { + triggerRules: policy.triggerRules, + confirmation: policy.confirmation, + recovery: policy.recovery, + } +} + +function normalizeApiIncidentPolicy(policy: Schemas['MonitorDto']['incidentPolicy']): unknown { + if (!policy) return null + return { + triggerRules: policy.triggerRules, + confirmation: policy.confirmation, + recovery: policy.recovery, + } +} + +// ── Dependency ────────────────────────────────────────────────────────── + +interface DependencySnapshot { + alertSensitivity: string | null + component: string | null +} + +const dependencyHandler = defineHandler({ + resourceType: 'dependency', + refType: 'dependencies', + configKey: 'dependencies', + listPath: '/api/v1/service-subscriptions', + + getRefKey: (yaml) => yaml.service, + getApiRefKey: (api) => api.slug ?? '', + getApiId: (api) => String(api.subscriptionId ?? ''), + + toDesiredSnapshot: (yaml, api) => ({ + alertSensitivity: yaml.alertSensitivity ?? api.alertSensitivity ?? null, + component: yaml.component ?? api.componentId ?? null, + }), + toCurrentSnapshot: (api) => ({ + alertSensitivity: api.alertSensitivity ?? null, + component: api.componentId ?? null, + }), + + fetchAll: (client) => fetchPaginated(client, '/api/v1/service-subscriptions'), + + async applyCreate(yaml, _refs, client) { + const resp = await typedPost>( + client, `/api/v1/service-subscriptions/${yaml.service}`, { + alertSensitivity: yaml.alertSensitivity ?? null, + componentId: yaml.component ?? null, + }, + ) + return resp.data?.subscriptionId ?? undefined + }, + async applyUpdate(yaml, id, _refs, client) { + if (yaml.alertSensitivity !== undefined) { + await typedPatch(client, `/api/v1/service-subscriptions/${id}/alert-sensitivity`, { + alertSensitivity: yaml.alertSensitivity, + }) + } + if (yaml.component !== undefined) { + await typedPatch(client, `/api/v1/service-subscriptions/${id}`, { + componentId: yaml.component, + }) + } + }, + deletePath: (id) => `/api/v1/service-subscriptions/${id}`, +}) + +// ── Handler registry ──────────────────────────────────────────────────── + +/** + * Compile-time complete map: TypeScript errors if any HandledResourceType is missing. + */ +export const HANDLER_MAP: Record = { + tag: tagHandler, + environment: environmentHandler, + secret: secretHandler, + alertChannel: alertChannelHandler, + notificationPolicy: notificationPolicyHandler, + webhook: webhookHandler, + resourceGroup: resourceGroupHandler, + monitor: monitorHandler, + dependency: dependencyHandler, +} + +export function getHandler(type: HandledResourceType): ResourceHandler { + return HANDLER_MAP[type] +} + +export function allHandlers(): ResourceHandler[] { + return Object.values(HANDLER_MAP) +} diff --git a/src/lib/yaml/index.ts b/src/lib/yaml/index.ts new file mode 100644 index 0000000..bfd5a98 --- /dev/null +++ b/src/lib/yaml/index.ts @@ -0,0 +1,13 @@ +export type {DevhelmConfig, YamlMonitor, YamlAlertChannel, YamlSectionKey} from './schema.js' +export {YAML_SECTION_KEYS} from './schema.js' +export {parseConfigFile, loadConfig, ParseError} from './parser.js' +export {validate} from './validator.js' +export type {ValidationResult, ValidationError} from './validator.js' +export {interpolate, findMissingVariables, InterpolationError} from './interpolation.js' +export {fetchAllRefs, ResolvedRefs} from './resolver.js' +export {diff, formatPlan} from './differ.js' +export type {Changeset, Change} from './differ.js' +export {apply} from './applier.js' +export type {ApplyResult} from './applier.js' +export {readState, writeState, buildState} from './state.js' +export * from './transform.js' diff --git a/src/lib/yaml/interpolation.ts b/src/lib/yaml/interpolation.ts new file mode 100644 index 0000000..4abca97 --- /dev/null +++ b/src/lib/yaml/interpolation.ts @@ -0,0 +1,86 @@ +/** + * Environment variable interpolation for YAML config values. + * + * Supports: + * ${VAR} — required, fails if unset + * ${VAR:-default} — with fallback value + * + * Interpolation runs on the raw YAML string before parsing, + * so it works in any value position (strings, URLs, etc.). + */ + +const ENV_VAR_PATTERN = /\$\{([^}]+)\}/g + +export class InterpolationError extends Error { + constructor( + public readonly variable: string, + message: string, + ) { + super(message) + this.name = 'InterpolationError' + } +} + +/** + * Interpolate all ${VAR} and ${VAR:-default} expressions in a string. + * Throws InterpolationError if a required variable is not set. + */ +export function interpolate(input: string, env: Record = process.env): string { + return input.replace(ENV_VAR_PATTERN, (_match, expr: string) => { + const separatorIdx = expr.indexOf(':-') + if (separatorIdx !== -1) { + const varName = expr.slice(0, separatorIdx) + const fallback = expr.slice(separatorIdx + 2) + const value = env[varName] + return value !== undefined && value !== '' ? value : fallback + } + + const varName = expr.trim() + const value = env[varName] + if (value === undefined || value === '') { + throw new InterpolationError( + varName, + `Environment variable \${${varName}} is required but not set. ` + + `Set it in your environment or use \${${varName}:-default} for a fallback.`, + ) + } + return value + }) +} + +/** + * Find all ${VAR} references in a string without resolving them. + * Returns variable names (without fallback info). + */ +export function findVariables(input: string): string[] { + const vars: string[] = [] + let match: RegExpExecArray | null + const re = new RegExp(ENV_VAR_PATTERN.source, 'g') + while ((match = re.exec(input)) !== null) { + const expr = match[1] + const separatorIdx = expr.indexOf(':-') + vars.push(separatorIdx !== -1 ? expr.slice(0, separatorIdx) : expr.trim()) + } + return vars +} + +/** + * Check which variables would fail during interpolation (no value, no default). + * Returns array of missing variable names. + */ +export function findMissingVariables(input: string, env: Record = process.env): string[] { + const missing: string[] = [] + const re = new RegExp(ENV_VAR_PATTERN.source, 'g') + let match: RegExpExecArray | null + while ((match = re.exec(input)) !== null) { + const expr = match[1] + const separatorIdx = expr.indexOf(':-') + if (separatorIdx === -1) { + const varName = expr.trim() + if (env[varName] === undefined || env[varName] === '') { + missing.push(varName) + } + } + } + return missing +} diff --git a/src/lib/yaml/parser.ts b/src/lib/yaml/parser.ts new file mode 100644 index 0000000..e7a689e --- /dev/null +++ b/src/lib/yaml/parser.ts @@ -0,0 +1,159 @@ +/** + * Parse a YAML string (or multiple files) into a typed DevhelmConfig. + * Handles env var interpolation, multi-file merging, and defaults application. + */ +import {readFileSync, existsSync, statSync, readdirSync} from 'node:fs' +import {join, resolve} from 'node:path' +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' + +export class ParseError extends Error { + constructor(message: string, public readonly file?: string) { + super(file ? `${file}: ${message}` : message) + this.name = 'ParseError' + } +} + +/** + * Load and parse a single YAML file with env var interpolation. + */ +export function parseConfigFile(filePath: string, resolveEnv = true): DevhelmConfig { + const absPath = resolve(filePath) + if (!existsSync(absPath)) { + throw new ParseError(`File not found: ${filePath}`) + } + + 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) + } catch (err) { + throw new ParseError(`Invalid YAML: ${err instanceof Error ? err.message : String(err)}`, filePath) + } + + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new ParseError('Config file is empty or not a YAML object', filePath) + } + + return parsed as DevhelmConfig +} + +/** + * Load config from one or more file paths (files or directories). + * Directories are scanned for *.yml and *.yaml files. + */ +export function loadConfig(paths: string[], resolveEnv = true): DevhelmConfig { + const files = expandPaths(paths) + if (files.length === 0) { + throw new ParseError('No YAML files found in the specified paths') + } + + if (files.length === 1) { + const config = parseConfigFile(files[0], resolveEnv) + return applyDefaults(config) + } + + const configs = files.map((f) => parseConfigFile(f, resolveEnv)) + const merged = mergeConfigs(configs) + return applyDefaults(merged) +} + +/** + * Expand file/directory paths into a flat list of .yml/.yaml files. + */ +function expandPaths(paths: string[]): string[] { + const files: string[] = [] + for (const p of paths) { + const absPath = resolve(p) + if (!existsSync(absPath)) { + throw new ParseError(`Path not found: ${p}`) + } + const stat = statSync(absPath) + if (stat.isDirectory()) { + const entries = readdirSync(absPath) + .filter((f) => f.endsWith('.yml') || f.endsWith('.yaml')) + .sort() + .map((f) => join(absPath, f)) + files.push(...entries) + } else { + files.push(absPath) + } + } + return files +} + +/** + * Merge multiple configs. Arrays are concatenated. Duplicate ref keys are caught by the validator. + * Uses YAML_SECTION_KEYS so adding a new section is a compile-time addition, not a manual edit here. + */ +function mergeConfigs(configs: DevhelmConfig[]): DevhelmConfig { + const merged: DevhelmConfig = {} + + for (const cfg of configs) { + if (cfg.version !== undefined) merged.version = cfg.version + + if (cfg.defaults) { + merged.defaults = merged.defaults ?? {} + if (cfg.defaults.monitors) { + merged.defaults.monitors = {...merged.defaults.monitors, ...cfg.defaults.monitors} + } + } + + for (const key of YAML_SECTION_KEYS) { + const items = cfg[key] + if (items) { + // TypeScript can't narrow cfg[key] through a generic dynamic index on a + // heterogeneous interface. YAML_SECTION_KEYS guarantees key ∈ DevhelmConfig + // and every value is T[] | undefined, so the concat is safe. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(merged as any)[key] = [...((merged as any)[key] ?? []), ...items] + } + } + } + + return merged +} + +/** + * Apply defaults.monitors to each monitor that doesn't override the field. + * Shallow merge: if monitor defines a field, it wins entirely (no deep merge). + */ +function applyDefaults(config: DevhelmConfig): DevhelmConfig { + const defaults = config.defaults?.monitors + if (!defaults || !config.monitors?.length) return config + + return { + ...config, + monitors: config.monitors.map((m) => applyMonitorDefaults(m, defaults)), + } +} + +function applyMonitorDefaults(monitor: YamlMonitor, defaults: YamlMonitorDefaults): YamlMonitor { + return { + ...monitor, + frequency: monitor.frequency ?? defaults.frequency, + enabled: monitor.enabled ?? defaults.enabled, + regions: monitor.regions ?? defaults.regions, + alertChannels: monitor.alertChannels ?? defaults.alertChannels, + incidentPolicy: monitor.incidentPolicy ?? defaults.incidentPolicy, + } +} diff --git a/src/lib/yaml/resolver.ts b/src/lib/yaml/resolver.ts new file mode 100644 index 0000000..e29db1b --- /dev/null +++ b/src/lib/yaml/resolver.ts @@ -0,0 +1,75 @@ +/** + * Reference resolver: fetches existing resources from the API via typed + * handler methods and builds name/slug → UUID maps for YAML reference resolution. + */ +import type {ApiClient} from '../api-client.js' +import type {RefType} from './types.js' +import {allHandlers} from './handlers.js' + +export type {RefType} + +interface RefEntry { + id: string + refKey: string + managedBy?: string + raw: Record +} + +export class ResolvedRefs { + private maps = new Map>() + + get(type: RefType, refKey: string): RefEntry | undefined { + return this.maps.get(type)?.get(refKey) + } + + resolve(type: RefType, refKey: string): string | undefined { + return this.get(type, refKey)?.id + } + + require(type: RefType, refKey: string): string { + const id = this.resolve(type, refKey) + if (!id) { + throw new Error(`Cannot resolve ${type} reference "${refKey}" — not found in YAML or API`) + } + return id + } + + set(type: RefType, refKey: string, entry: RefEntry): void { + if (!this.maps.has(type)) this.maps.set(type, new Map()) + this.maps.get(type)!.set(refKey, entry) + } + + all(type: RefType): Map { + return this.maps.get(type) ?? new Map() + } + + allEntries(type: RefType): RefEntry[] { + return [...this.all(type).values()] + } +} + +/** + * Fetch all resources from the API via handler.fetchAll() and build + * reference maps using handler metadata (getApiRefKey, getApiId, etc.). + */ +export async function fetchAllRefs(client: ApiClient): Promise { + const refs = new ResolvedRefs() + const handlers = allHandlers() + + const results = await Promise.all(handlers.map((h) => h.fetchAll(client))) + + for (let i = 0; i < handlers.length; i++) { + const handler = handlers[i] + for (const item of results[i]) { + const refKey = handler.getApiRefKey(item) + refs.set(handler.refType, refKey, { + id: handler.getApiId(item), + refKey, + managedBy: handler.getManagedBy?.(item), + raw: item as Record, + }) + } + } + + return refs +} diff --git a/src/lib/yaml/schema.ts b/src/lib/yaml/schema.ts new file mode 100644 index 0000000..c220309 --- /dev/null +++ b/src/lib/yaml/schema.ts @@ -0,0 +1,381 @@ +/** + * YAML configuration schema types — derived from OpenAPI-generated API types. + * + * These types define what users write in devhelm.yml. They mirror API request + * types closely, but replace UUIDs with name/slug references and use friendlier + * field names (e.g. `frequency` instead of `frequencySeconds`). + * + * The transform layer (transform.ts) maps these to API request types with + * compile-time type checking on both sides. + */ +import type {components} from '../api.generated.js' + +type Schemas = components['schemas'] + +// ── Re-export API types used directly in YAML (no transformation needed) ── + +export type MonitorType = Schemas['CreateMonitorRequest']['type'] +export type HttpMethod = Schemas['HttpMonitorConfig']['method'] +export type DnsRecordType = NonNullable[number]> +export type AssertionSeverity = NonNullable +export type TriggerRuleType = Schemas['TriggerRule']['type'] +export type TriggerRuleScope = Schemas['TriggerRule']['scope'] +export type TriggerRuleSeverity = Schemas['TriggerRule']['severity'] +export type TriggerAggregation = NonNullable +export type ComparisonOperator = Schemas['StatusCodeAssertion'] extends {type: string} & infer R + ? R extends {operator: infer O} ? O : never + : never + +// ── Enum constants for validation ────────────────────────────────────── + +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 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 MIN_FREQUENCY = 30 +export const MAX_FREQUENCY = 86400 + +// ── Assertion type names (discriminator values) ──────────────────────── + +export const ASSERTION_TYPES = [ + 'StatusCodeAssertion', 'ResponseTimeAssertion', 'ResponseTimeWarnAssertion', + 'BodyContainsAssertion', 'RegexBodyAssertion', 'HeaderValueAssertion', + 'JsonPathAssertion', 'SslExpiryAssertion', 'ResponseSizeAssertion', + 'RedirectCountAssertion', 'RedirectTargetAssertion', + 'DnsResolvesAssertion', 'DnsResponseTimeAssertion', 'DnsResponseTimeWarnAssertion', + 'DnsExpectedIpsAssertion', 'DnsExpectedCnameAssertion', + 'DnsRecordContainsAssertion', 'DnsRecordEqualsAssertion', + 'DnsTxtContainsAssertion', 'DnsMinAnswersAssertion', 'DnsMaxAnswersAssertion', + 'DnsTtlLowAssertion', 'DnsTtlHighAssertion', + 'TcpConnectsAssertion', 'TcpResponseTimeAssertion', 'TcpResponseTimeWarnAssertion', + 'IcmpReachableAssertion', 'IcmpResponseTimeAssertion', 'IcmpResponseTimeWarnAssertion', + 'IcmpPacketLossAssertion', + 'HeartbeatReceivedAssertion', 'HeartbeatMaxIntervalAssertion', + 'HeartbeatIntervalDriftAssertion', 'HeartbeatPayloadContainsAssertion', + 'McpConnectsAssertion', 'McpResponseTimeAssertion', 'McpResponseTimeWarnAssertion', + 'McpHasCapabilityAssertion', 'McpToolAvailableAssertion', + 'McpMinToolsAssertion', 'McpProtocolVersionAssertion', 'McpToolCountChangedAssertion', +] as const + +export type AssertionType = (typeof ASSERTION_TYPES)[number] + +// ── Monitor config types (YAML mirrors API directly) ─────────────────── + +export interface YamlHttpConfig { + url: string + method: HttpMethod + customHeaders?: Record + requestBody?: string + contentType?: string + verifyTls?: boolean +} + +export interface YamlDnsConfig { + hostname: string + recordTypes?: DnsRecordType[] + nameservers?: string[] + timeoutMs?: number + totalTimeoutMs?: number +} + +export interface YamlTcpConfig { + host: string + port?: number + timeoutMs?: number +} + +export interface YamlIcmpConfig { + host: string + packetCount?: number + timeoutMs?: number +} + +export interface YamlHeartbeatConfig { + expectedInterval: number + gracePeriod: number +} + +export interface YamlMcpServerConfig { + command: string + args?: string[] + env?: Record +} + +export type YamlMonitorConfig = + | YamlHttpConfig + | YamlDnsConfig + | YamlTcpConfig + | YamlIcmpConfig + | YamlHeartbeatConfig + | YamlMcpServerConfig + +// ── Assertion config (YAML mirrors API discriminated union) ──────────── + +export interface YamlAssertion { + type: AssertionType + config?: Record + severity?: AssertionSeverity +} + +// ── Auth config (with vault secret name reference) ───────────────────── + +export interface YamlBearerAuth { + type: 'BearerAuthConfig' + secret: string +} + +export interface YamlBasicAuth { + type: 'BasicAuthConfig' + secret: string +} + +export interface YamlApiKeyAuth { + type: 'ApiKeyAuthConfig' + headerName: string + secret: string +} + +export interface YamlHeaderAuth { + type: 'HeaderAuthConfig' + headerName: string + secret: string +} + +export type YamlAuth = YamlBearerAuth | YamlBasicAuth | YamlApiKeyAuth | YamlHeaderAuth + +// ── Incident policy (YAML mirrors API types directly) ────────────────── + +export interface YamlTriggerRule { + type: TriggerRuleType + count?: number + windowMinutes?: number + scope: TriggerRuleScope + thresholdMs?: number + severity: TriggerRuleSeverity + aggregationType?: TriggerAggregation +} + +export interface YamlConfirmationPolicy { + type: 'multi_region' + minRegionsFailing?: number + maxWaitSeconds?: number +} + +export interface YamlRecoveryPolicy { + consecutiveSuccesses?: number + minRegionsPassing?: number + cooldownMinutes?: number +} + +export interface YamlIncidentPolicy { + triggerRules: YamlTriggerRule[] + confirmation: YamlConfirmationPolicy + recovery: YamlRecoveryPolicy +} + +// ── Escalation (with channel name references) ────────────────────────── + +export interface YamlEscalationStep { + channels: string[] + delayMinutes?: number + requireAck?: boolean + repeatIntervalSeconds?: number +} + +export interface YamlEscalationChain { + steps: YamlEscalationStep[] + onResolve?: string + onReopen?: string +} + +// ── Match rules for notification policies ────────────────────────────── + +export interface YamlMatchRule { + type: string + value?: string + monitorNames?: string[] + regions?: string[] + values?: string[] +} + +// ── Channel configs (YAML uses lowercase type + flat config) ─────────── + +export interface YamlSlackConfig { + webhookUrl: string + mentionText?: string +} + +export interface YamlDiscordConfig { + webhookUrl: string + mentionRoleId?: string +} + +export interface YamlEmailConfig { + recipients: string[] +} + +export interface YamlWebhookConfig { + url: string + signingSecret?: string + customHeaders?: Record +} + +export interface YamlPagerDutyConfig { + routingKey: string + severityOverride?: string +} + +export interface YamlOpsGenieConfig { + apiKey: string + region?: string +} + +export interface YamlTeamsConfig { + webhookUrl: string +} + +export type YamlChannelConfig = + | YamlSlackConfig + | YamlDiscordConfig + | YamlEmailConfig + | YamlWebhookConfig + | YamlPagerDutyConfig + | YamlOpsGenieConfig + | YamlTeamsConfig + +// ── Retry strategy (for resource groups) ─────────────────────────────── + +export interface YamlRetryStrategy { + type: string + maxRetries?: number + interval?: number +} + +// ── Top-level YAML resource types ────────────────────────────────────── + +export interface YamlTag { + name: string + color?: string +} + +export interface YamlEnvironment { + name: string + slug: string + variables?: Record + isDefault?: boolean +} + +export interface YamlSecret { + key: string + value: string +} + +export interface YamlAlertChannel { + name: string + type: ChannelType + config: YamlChannelConfig +} + +export interface YamlNotificationPolicy { + name: string + enabled?: boolean + priority?: number + matchRules?: YamlMatchRule[] + escalation: YamlEscalationChain +} + +export interface YamlWebhook { + url: string + events: string[] + description?: string +} + +export interface YamlResourceGroup { + name: string + description?: string + alertPolicy?: string + defaultFrequency?: number + defaultRegions?: string[] + defaultRetryStrategy?: YamlRetryStrategy + defaultAlertChannels?: string[] + defaultEnvironment?: string + healthThresholdType?: (typeof HEALTH_THRESHOLD_TYPES)[number] + healthThresholdValue?: number + suppressMemberAlerts?: boolean + confirmationDelaySeconds?: number + recoveryCooldownMinutes?: number + monitors?: string[] + services?: string[] +} + +export interface YamlMonitor { + name: string + type: MonitorType + config: YamlMonitorConfig + frequency?: number + enabled?: boolean + regions?: string[] + environment?: string + tags?: string[] + alertChannels?: string[] + assertions?: YamlAssertion[] + auth?: YamlAuth + incidentPolicy?: YamlIncidentPolicy + resourceGroup?: string +} + +export interface YamlDependency { + service: string + alertSensitivity?: (typeof ALERT_SENSITIVITIES)[number] + component?: string +} + +// ── Defaults section ─────────────────────────────────────────────────── + +export interface YamlMonitorDefaults { + frequency?: number + enabled?: boolean + regions?: string[] + alertChannels?: string[] + incidentPolicy?: YamlIncidentPolicy +} + +export interface YamlDefaults { + monitors?: YamlMonitorDefaults +} + +// ── Top-level config ─────────────────────────────────────────────────── + +export interface DevhelmConfig { + version?: string + defaults?: YamlDefaults + tags?: YamlTag[] + environments?: YamlEnvironment[] + secrets?: YamlSecret[] + alertChannels?: YamlAlertChannel[] + notificationPolicies?: YamlNotificationPolicy[] + webhooks?: YamlWebhook[] + resourceGroups?: YamlResourceGroup[] + monitors?: YamlMonitor[] + dependencies?: YamlDependency[] +} + +// ── Section keys (for parity enforcement) ────────────────────────────── + +export const YAML_SECTION_KEYS = [ + 'tags', 'environments', 'secrets', 'alertChannels', + 'notificationPolicies', 'webhooks', 'resourceGroups', + 'monitors', 'dependencies', +] as const + +export type YamlSectionKey = (typeof YAML_SECTION_KEYS)[number] diff --git a/src/lib/yaml/state.ts b/src/lib/yaml/state.ts new file mode 100644 index 0000000..49dce0a --- /dev/null +++ b/src/lib/yaml/state.ts @@ -0,0 +1,58 @@ +/** + * Local state file for tracking which resources were created by `devhelm deploy`. + * Used for pruning: only delete resources that we manage. + * + * State file: .devhelm/state.json (gitignored by convention) + */ +import {existsSync, readFileSync, writeFileSync, mkdirSync} from 'node:fs' +import {join, dirname} from 'node:path' + +export interface StateEntry { + resourceType: string + refKey: string + id: string + createdAt: string +} + +export interface DeployState { + version: string + lastDeployedAt: string + resources: StateEntry[] +} + +const STATE_DIR = '.devhelm' +const STATE_FILE = 'state.json' +export const STATE_VERSION = '1' + +function statePath(cwd: string): string { + return join(cwd, STATE_DIR, STATE_FILE) +} + +export function readState(cwd: string = process.cwd()): DeployState | undefined { + const path = statePath(cwd) + if (!existsSync(path)) return undefined + try { + return JSON.parse(readFileSync(path, 'utf8')) as DeployState + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.warn(`Warning: corrupt state file at ${path} (${msg}). Treating as fresh state.`) + return undefined + } +} + +export function writeState(state: DeployState, cwd: string = process.cwd()): void { + const path = statePath(cwd) + const dir = dirname(path) + if (!existsSync(dir)) { + mkdirSync(dir, {recursive: true}) + } + writeFileSync(path, JSON.stringify(state, null, 2)) +} + +export function buildState(entries: StateEntry[]): DeployState { + return { + version: STATE_VERSION, + lastDeployedAt: new Date().toISOString(), + resources: entries, + } +} diff --git a/src/lib/yaml/transform.ts b/src/lib/yaml/transform.ts new file mode 100644 index 0000000..992f8dc --- /dev/null +++ b/src/lib/yaml/transform.ts @@ -0,0 +1,232 @@ +/** + * Type-checked transforms: YAML config types → API request types. + * Every function here is compile-time verified against both sides. + */ +import type {components} from '../api.generated.js' +import type { + YamlMonitor, YamlAlertChannel, YamlNotificationPolicy, + YamlResourceGroup, YamlWebhook, YamlTag, YamlEnvironment, + YamlSecret, YamlAssertion, YamlAuth, + YamlIncidentPolicy, YamlEscalationStep, YamlMatchRule, + ChannelType, +} from './schema.js' +import type {ResolvedRefs} from './resolver.js' + +type Schemas = components['schemas'] + +// ── Channel type discriminator mapping ───────────────────────────────── + +const CHANNEL_TYPE_DISCRIMINATOR: Record = { + slack: 'SlackChannelConfig', + discord: 'DiscordChannelConfig', + email: 'EmailChannelConfig', + webhook: 'WebhookChannelConfig', + pagerduty: 'PagerDutyChannelConfig', + opsgenie: 'OpsGenieChannelConfig', + teams: 'TeamsChannelConfig', +} + +// ── Tag ──────────────────────────────────────────────────────────────── + +export function toCreateTagRequest(tag: YamlTag): Schemas['CreateTagRequest'] { + return { + name: tag.name, + color: tag.color ?? null, + } +} + +// ── Environment ──────────────────────────────────────────────────────── + +export function toCreateEnvironmentRequest(env: YamlEnvironment): Schemas['CreateEnvironmentRequest'] { + return { + name: env.name, + slug: env.slug, + variables: env.variables ?? null, + isDefault: env.isDefault, + } +} + +// ── Secret ───────────────────────────────────────────────────────────── + +export function toCreateSecretRequest(secret: YamlSecret): Schemas['CreateSecretRequest'] { + return {key: secret.key, value: secret.value} +} + +// ── Alert Channel ────────────────────────────────────────────────────── + +export function toCreateAlertChannelRequest(channel: YamlAlertChannel): Schemas['CreateAlertChannelRequest'] { + const channelType = CHANNEL_TYPE_DISCRIMINATOR[channel.type] + const config = {channelType, ...channel.config} as Schemas['CreateAlertChannelRequest']['config'] + return {name: channel.name, config} +} + +// ── Notification Policy ──────────────────────────────────────────────── + +export function toCreateNotificationPolicyRequest( + policy: YamlNotificationPolicy, + refs: ResolvedRefs, +): Schemas['CreateNotificationPolicyRequest'] { + return { + name: policy.name, + enabled: policy.enabled ?? true, + priority: policy.priority ?? 0, + matchRules: policy.matchRules?.map((r) => toMatchRule(r, refs)), + escalation: { + steps: policy.escalation.steps.map((s) => toEscalationStep(s, refs)), + onResolve: policy.escalation.onResolve ?? null, + onReopen: policy.escalation.onReopen ?? null, + }, + } +} + +function toEscalationStep(step: YamlEscalationStep, refs: ResolvedRefs): Schemas['EscalationStep'] { + return { + channelIds: step.channels.map((name) => refs.require('alertChannels', name)), + delayMinutes: step.delayMinutes ?? 0, + requireAck: step.requireAck ?? null, + repeatIntervalSeconds: step.repeatIntervalSeconds ?? null, + } +} + +function toMatchRule(rule: YamlMatchRule, refs: ResolvedRefs): Schemas['MatchRule'] { + return { + type: rule.type, + value: rule.value ?? null, + monitorIds: rule.monitorNames?.map((name) => refs.require('monitors', name)) ?? null, + regions: rule.regions ?? null, + values: rule.values ?? null, + } +} + +// ── Webhook ──────────────────────────────────────────────────────────── + +export function toCreateWebhookRequest(webhook: YamlWebhook): Schemas['CreateWebhookEndpointRequest'] { + return { + url: webhook.url, + subscribedEvents: webhook.events, + description: webhook.description, + } +} + +// ── Resource Group ───────────────────────────────────────────────────── + +export function toCreateResourceGroupRequest( + group: YamlResourceGroup, + refs: ResolvedRefs, +): Schemas['CreateResourceGroupRequest'] { + return { + name: group.name, + description: group.description ?? null, + alertPolicyId: group.alertPolicy ? refs.resolve('notificationPolicies', group.alertPolicy) ?? null : null, + defaultFrequency: group.defaultFrequency ?? null, + defaultRegions: group.defaultRegions ?? null, + defaultRetryStrategy: group.defaultRetryStrategy ? { + type: group.defaultRetryStrategy.type, + maxRetries: group.defaultRetryStrategy.maxRetries, + interval: group.defaultRetryStrategy.interval, + } : undefined, + defaultAlertChannels: group.defaultAlertChannels?.map((n) => refs.resolve('alertChannels', n) ?? n) ?? null, + defaultEnvironmentId: group.defaultEnvironment ? refs.resolve('environments', group.defaultEnvironment) ?? null : null, + healthThresholdType: group.healthThresholdType ?? null, + healthThresholdValue: group.healthThresholdValue ?? null, + suppressMemberAlerts: group.suppressMemberAlerts, + confirmationDelaySeconds: group.confirmationDelaySeconds ?? null, + recoveryCooldownMinutes: group.recoveryCooldownMinutes ?? null, + } +} + +// ── Monitor ──────────────────────────────────────────────────────────── + +export function toCreateMonitorRequest( + monitor: YamlMonitor, + refs: ResolvedRefs, +): Schemas['CreateMonitorRequest'] { + return { + name: monitor.name, + type: monitor.type, + config: monitor.config as Schemas['CreateMonitorRequest']['config'], + managedBy: 'CLI', + frequencySeconds: monitor.frequency, + enabled: monitor.enabled, + regions: monitor.regions ?? null, + environmentId: monitor.environment ? refs.resolve('environments', monitor.environment) ?? null : null, + assertions: monitor.assertions?.map(toCreateAssertionRequest) ?? null, + auth: monitor.auth ? toAuthConfig(monitor.auth, refs) : undefined, + incidentPolicy: monitor.incidentPolicy ? toIncidentPolicy(monitor.incidentPolicy) : undefined, + alertChannelIds: monitor.alertChannels?.map((n) => refs.require('alertChannels', n)) ?? null, + tags: monitor.tags ? { + tagIds: monitor.tags.map((n) => refs.resolve('tags', n)).filter((id): id is string => id !== undefined), + newTags: monitor.tags + .filter((n) => !refs.resolve('tags', n)) + .map((n) => ({name: n})), + } : undefined, + } +} + +export function toUpdateMonitorRequest( + monitor: YamlMonitor, + refs: ResolvedRefs, +): Schemas['UpdateMonitorRequest'] { + return { + name: monitor.name, + config: monitor.config as Schemas['UpdateMonitorRequest']['config'], + managedBy: 'CLI', + frequencySeconds: monitor.frequency, + enabled: monitor.enabled, + regions: monitor.regions ?? null, + environmentId: monitor.environment ? refs.resolve('environments', monitor.environment) ?? null : null, + assertions: monitor.assertions?.map(toCreateAssertionRequest) ?? null, + auth: monitor.auth ? toAuthConfig(monitor.auth, refs) : undefined, + incidentPolicy: monitor.incidentPolicy ? toIncidentPolicy(monitor.incidentPolicy) : undefined, + alertChannelIds: monitor.alertChannels?.map((n) => refs.require('alertChannels', n)) ?? null, + tags: monitor.tags ? { + tagIds: monitor.tags.map((n) => refs.resolve('tags', n)).filter((id): id is string => id !== undefined), + newTags: monitor.tags + .filter((n) => !refs.resolve('tags', n)) + .map((n) => ({name: n})), + } : undefined, + } +} + +function toCreateAssertionRequest(a: YamlAssertion): Schemas['CreateAssertionRequest'] { + const config = {type: a.type, ...(a.config ?? {})} as Schemas['CreateAssertionRequest']['config'] + return {config, severity: a.severity} +} + +function toAuthConfig(auth: YamlAuth, refs: ResolvedRefs): Schemas['CreateMonitorRequest']['auth'] { + const secretId = refs.resolve('secrets', auth.secret) ?? undefined + switch (auth.type) { + case 'BearerAuthConfig': + return {type: 'BearerAuthConfig', vaultSecretId: secretId ?? null} as Schemas['BearerAuthConfig'] + case 'BasicAuthConfig': + return {type: 'BasicAuthConfig', vaultSecretId: secretId ?? null} as Schemas['BasicAuthConfig'] + case 'ApiKeyAuthConfig': + return {type: 'ApiKeyAuthConfig', headerName: auth.headerName, vaultSecretId: secretId ?? null} as Schemas['ApiKeyAuthConfig'] + case 'HeaderAuthConfig': + return {type: 'HeaderAuthConfig', headerName: auth.headerName, vaultSecretId: secretId ?? null} as Schemas['HeaderAuthConfig'] + } +} + +function toIncidentPolicy(policy: YamlIncidentPolicy): Schemas['UpdateIncidentPolicyRequest'] { + return { + triggerRules: policy.triggerRules.map((r) => ({ + type: r.type, + count: r.count ?? null, + windowMinutes: r.windowMinutes ?? null, + scope: r.scope, + thresholdMs: r.thresholdMs ?? null, + severity: r.severity, + aggregationType: r.aggregationType ?? null, + })), + confirmation: { + type: policy.confirmation.type, + minRegionsFailing: policy.confirmation.minRegionsFailing, + maxWaitSeconds: policy.confirmation.maxWaitSeconds, + }, + recovery: { + consecutiveSuccesses: policy.recovery.consecutiveSuccesses, + minRegionsPassing: policy.recovery.minRegionsPassing, + cooldownMinutes: policy.recovery.cooldownMinutes, + }, + } +} diff --git a/src/lib/yaml/types.ts b/src/lib/yaml/types.ts new file mode 100644 index 0000000..5218d08 --- /dev/null +++ b/src/lib/yaml/types.ts @@ -0,0 +1,48 @@ +/** + * Shared type definitions for the YAML engine. + * + * Extracted into a standalone module to avoid circular dependencies + * between handlers, differ, resolver, and applier. + */ + +export type ChangeAction = 'create' | 'update' | 'delete' + +export type ResourceType = + | 'tag' | 'environment' | 'secret' | 'alertChannel' + | 'notificationPolicy' | 'webhook' | 'resourceGroup' + | 'monitor' | 'dependency' | 'groupMembership' + +/** Resource types that have a full ResourceHandler implementation. */ +export type HandledResourceType = Exclude + +export type RefType = + | 'tags' | 'environments' | 'secrets' | 'alertChannels' + | 'notificationPolicies' | 'webhooks' | 'resourceGroups' + | 'monitors' | 'dependencies' + +export interface Change { + action: ChangeAction + resourceType: ResourceType + refKey: string + existingId?: string + desired?: unknown + current?: unknown +} + +export interface DiffOptions { + prune?: boolean +} + +export interface Changeset { + creates: Change[] + updates: Change[] + deletes: Change[] + memberships: Change[] +} + +/** Dependency order for topological sort (creates ascending, deletes descending). */ +export const RESOURCE_ORDER: ResourceType[] = [ + 'tag', 'environment', 'secret', 'alertChannel', + 'notificationPolicy', 'webhook', 'resourceGroup', + 'monitor', 'dependency', 'groupMembership', +] diff --git a/src/lib/yaml/validator.ts b/src/lib/yaml/validator.ts new file mode 100644 index 0000000..54fdc51 --- /dev/null +++ b/src/lib/yaml/validator.ts @@ -0,0 +1,483 @@ +/** + * Deep offline validation of a DevhelmConfig against schema constraints. + * Checks types, required fields, enum values, frequency bounds, + * intra-YAML reference integrity, and duplicate ref keys. + */ +import type { + DevhelmConfig, YamlMonitor, YamlAlertChannel, YamlNotificationPolicy, + YamlResourceGroup, YamlWebhook, YamlTag, YamlEnvironment, YamlSecret, + YamlDependency, YamlAssertion, YamlAuth, YamlIncidentPolicy, + YamlEscalationStep, YamlMatchRule, + YamlChannelConfig, YamlMonitorConfig, +} from './schema.js' +import { + MONITOR_TYPES, HTTP_METHODS, DNS_RECORD_TYPES, + ASSERTION_TYPES, ASSERTION_SEVERITIES, COMPARISON_OPERATORS, + CHANNEL_TYPES, ALERT_SENSITIVITIES, HEALTH_THRESHOLD_TYPES, + TRIGGER_RULE_TYPES, TRIGGER_SCOPES, TRIGGER_SEVERITIES, TRIGGER_AGGREGATIONS, + MIN_FREQUENCY, MAX_FREQUENCY, +} from './schema.js' + +export interface ValidationError { + path: string + message: string +} + +export interface ValidationResult { + errors: ValidationError[] + warnings: ValidationError[] +} + +export function validate(config: DevhelmConfig): ValidationResult { + const ctx = new ValidationContext() + validateConfig(config, ctx) + return {errors: ctx.errors, warnings: ctx.warnings} +} + +class ValidationContext { + errors: ValidationError[] = [] + warnings: ValidationError[] = [] + + private declaredNames = new Map>() + + error(path: string, message: string): void { + this.errors.push({path, message}) + } + + warn(path: string, message: string): void { + this.warnings.push({path, message}) + } + + declareRef(type: string, name: string, path: string): void { + if (!this.declaredNames.has(type)) { + this.declaredNames.set(type, new Set()) + } + const set = this.declaredNames.get(type)! + if (set.has(name)) { + this.error(path, `Duplicate ${type} name "${name}" — names must be unique within each resource type`) + } + set.add(name) + } + + hasRef(type: string, name: string): boolean { + return this.declaredNames.get(type)?.has(name) ?? false + } + + checkRef(refType: string, name: string, path: string): void { + if (!this.hasRef(refType, name)) { + this.warn(path, `Reference "${name}" not found in YAML ${refType} definitions. It must exist in the API at deploy time.`) + } + } +} + +// ── Top-level config validation ──────────────────────────────────────── + +function validateConfig(config: DevhelmConfig, ctx: ValidationContext): void { + if (config.version !== undefined && config.version !== '1') { + ctx.warn('version', `Unknown config version "${config.version}". Supported: "1"`) + } + + const hasAnyResource = config.tags?.length || config.environments?.length || + config.secrets?.length || config.alertChannels?.length || + config.notificationPolicies?.length || config.webhooks?.length || + config.resourceGroups?.length || config.monitors?.length || + config.dependencies?.length + + if (!hasAnyResource) { + ctx.error('', 'Config has no resource definitions. Add at least one section (monitors, tags, etc.)') + } + + collectDeclarations(config, ctx) + + if (config.tags) validateArray(config.tags, 'tags', ctx, validateTag) + if (config.environments) validateArray(config.environments, 'environments', ctx, validateEnvironment) + if (config.secrets) validateArray(config.secrets, 'secrets', ctx, validateSecretDef) + if (config.alertChannels) validateArray(config.alertChannels, 'alertChannels', ctx, validateAlertChannel) + if (config.notificationPolicies) validateArray(config.notificationPolicies, 'notificationPolicies', ctx, validateNotificationPolicy) + if (config.webhooks) validateArray(config.webhooks, 'webhooks', ctx, validateWebhookDef) + if (config.resourceGroups) validateArray(config.resourceGroups, 'resourceGroups', ctx, validateResourceGroup) + if (config.monitors) validateArray(config.monitors, 'monitors', ctx, validateMonitor) + if (config.dependencies) validateArray(config.dependencies, 'dependencies', ctx, validateDependency) +} + +function collectDeclarations(config: DevhelmConfig, ctx: ValidationContext): void { + for (const t of config.tags ?? []) if (t.name) ctx.declareRef('tags', t.name, 'tags') + for (const e of config.environments ?? []) if (e.slug) ctx.declareRef('environments', e.slug, 'environments') + for (const s of config.secrets ?? []) if (s.key) ctx.declareRef('secrets', s.key, 'secrets') + for (const c of config.alertChannels ?? []) if (c.name) ctx.declareRef('alertChannels', c.name, 'alertChannels') + for (const p of config.notificationPolicies ?? []) if (p.name) ctx.declareRef('notificationPolicies', p.name, 'notificationPolicies') + for (const w of config.webhooks ?? []) if (w.url) ctx.declareRef('webhooks', w.url, 'webhooks') + for (const g of config.resourceGroups ?? []) if (g.name) ctx.declareRef('resourceGroups', g.name, 'resourceGroups') + for (const m of config.monitors ?? []) if (m.name) ctx.declareRef('monitors', m.name, 'monitors') + for (const d of config.dependencies ?? []) if (d.service) ctx.declareRef('dependencies', d.service, 'dependencies') +} + +// ── Generic array validator ──────────────────────────────────────────── + +function validateArray( + items: T[], + section: string, + ctx: ValidationContext, + itemValidator: (item: T, path: string, ctx: ValidationContext) => void, +): void { + if (!Array.isArray(items)) { + ctx.error(section, `"${section}" must be an array`) + return + } + for (let i = 0; i < items.length; i++) { + itemValidator(items[i], `${section}[${i}]`, ctx) + } +} + +// ── Individual resource validators ───────────────────────────────────── + +function validateTag(tag: YamlTag, path: string, ctx: ValidationContext): void { + requireString(tag, 'name', path, ctx) + if (tag.color !== undefined && typeof tag.color === 'string' && !/^#[0-9a-fA-F]{6}$/.test(tag.color)) { + ctx.warn(`${path}.color`, 'Color should be a hex code like #FF0000') + } +} + +function validateEnvironment(env: YamlEnvironment, path: string, ctx: ValidationContext): void { + requireString(env, 'name', path, ctx) + requireString(env, 'slug', path, ctx) + if (env.slug && !/^[a-z0-9_-]+$/.test(env.slug)) { + ctx.error(`${path}.slug`, 'Slug must be lowercase alphanumeric with hyphens and underscores') + } +} + +function validateSecretDef(secret: YamlSecret, path: string, ctx: ValidationContext): void { + requireString(secret, 'key', path, ctx) + requireString(secret, 'value', path, ctx) +} + +function validateAlertChannel(channel: YamlAlertChannel, path: string, ctx: ValidationContext): void { + requireString(channel, 'name', path, ctx) + if (!channel.type) { + ctx.error(`${path}.type`, '"type" is required') + } else if (!CHANNEL_TYPES.includes(channel.type)) { + ctx.error(`${path}.type`, `Invalid channel type "${channel.type}". Must be one of: ${CHANNEL_TYPES.join(', ')}`) + } + if (!channel.config || typeof channel.config !== 'object') { + ctx.error(`${path}.config`, '"config" is required and must be an object') + return + } + validateChannelConfig(channel.type, channel.config, `${path}.config`, ctx) +} + +function validateChannelConfig(type: string, config: YamlChannelConfig, path: string, ctx: ValidationContext): void { + switch (type) { + case 'slack': + case 'discord': + case 'teams': + if (!('webhookUrl' in config) || !config.webhookUrl) { + ctx.error(`${path}.webhookUrl`, `${type.charAt(0).toUpperCase() + type.slice(1)} channel requires "webhookUrl"`) + } + break + case 'email': + if (!('recipients' in config) || !Array.isArray(config.recipients) || config.recipients.length === 0) { + ctx.error(`${path}.recipients`, 'Email channel requires "recipients" array with at least one address') + } + break + case 'pagerduty': + if (!('routingKey' in config) || !config.routingKey) { + ctx.error(`${path}.routingKey`, 'PagerDuty channel requires "routingKey"') + } + break + case 'opsgenie': + if (!('apiKey' in config) || !config.apiKey) { + ctx.error(`${path}.apiKey`, 'OpsGenie channel requires "apiKey"') + } + break + case 'webhook': + if (!('url' in config) || !config.url) { + ctx.error(`${path}.url`, 'Webhook channel requires "url"') + } + break + } +} + +function validateNotificationPolicy(policy: YamlNotificationPolicy, path: string, ctx: ValidationContext): void { + requireString(policy, 'name', path, ctx) + + if (!policy.escalation) { + ctx.error(`${path}.escalation`, '"escalation" is required') + } else { + if (!policy.escalation.steps || !Array.isArray(policy.escalation.steps) || policy.escalation.steps.length === 0) { + ctx.error(`${path}.escalation.steps`, 'Escalation must have at least one step') + } else { + for (let i = 0; i < policy.escalation.steps.length; i++) { + validateEscalationStep(policy.escalation.steps[i], `${path}.escalation.steps[${i}]`, ctx) + } + } + } + + if (policy.matchRules) { + for (let i = 0; i < policy.matchRules.length; i++) { + validateMatchRule(policy.matchRules[i], `${path}.matchRules[${i}]`, ctx) + } + } + + if (policy.priority !== undefined && (typeof policy.priority !== 'number' || policy.priority < 0)) { + ctx.error(`${path}.priority`, 'Priority must be a non-negative number') + } +} + +function validateEscalationStep(step: YamlEscalationStep, path: string, ctx: ValidationContext): void { + if (!step.channels || !Array.isArray(step.channels) || step.channels.length === 0) { + ctx.error(`${path}.channels`, 'Each escalation step must have at least one channel') + } else { + for (const name of step.channels) { + ctx.checkRef('alertChannels', name, `${path}.channels`) + } + } + if (step.delayMinutes !== undefined && (typeof step.delayMinutes !== 'number' || step.delayMinutes < 0)) { + ctx.error(`${path}.delayMinutes`, 'delayMinutes must be a non-negative number') + } +} + +function validateMatchRule(rule: YamlMatchRule, path: string, ctx: ValidationContext): void { + if (!rule.type) { + ctx.error(`${path}.type`, 'Match rule requires "type"') + } + if (rule.monitorNames) { + for (const name of rule.monitorNames) { + ctx.checkRef('monitors', name, `${path}.monitorNames`) + } + } +} + +function validateWebhookDef(webhook: YamlWebhook, path: string, ctx: ValidationContext): void { + requireString(webhook, 'url', path, ctx) + if (!webhook.events || !Array.isArray(webhook.events) || webhook.events.length === 0) { + ctx.error(`${path}.events`, '"events" is required and must be a non-empty array') + } +} + +function validateResourceGroup(group: YamlResourceGroup, path: string, ctx: ValidationContext): void { + requireString(group, 'name', path, ctx) + + if (group.healthThresholdType && !HEALTH_THRESHOLD_TYPES.includes(group.healthThresholdType as typeof HEALTH_THRESHOLD_TYPES[number])) { + ctx.error(`${path}.healthThresholdType`, `Must be one of: ${HEALTH_THRESHOLD_TYPES.join(', ')}`) + } + + if (group.defaultFrequency !== undefined) { + validateFrequency(group.defaultFrequency, `${path}.defaultFrequency`, ctx) + } + + if (group.monitors) { + for (const name of group.monitors) { + ctx.checkRef('monitors', name, `${path}.monitors`) + } + } + if (group.services) { + for (const slug of group.services) { + ctx.checkRef('dependencies', slug, `${path}.services`) + } + } + if (group.defaultAlertChannels) { + for (const name of group.defaultAlertChannels) { + ctx.checkRef('alertChannels', name, `${path}.defaultAlertChannels`) + } + } + if (group.defaultEnvironment) { + ctx.checkRef('environments', group.defaultEnvironment, `${path}.defaultEnvironment`) + } + if (group.alertPolicy) { + ctx.checkRef('notificationPolicies', group.alertPolicy, `${path}.alertPolicy`) + } +} + +function validateMonitor(monitor: YamlMonitor, path: string, ctx: ValidationContext): void { + requireString(monitor, 'name', path, ctx) + + if (!monitor.type) { + ctx.error(`${path}.type`, '"type" is required') + } else if (!MONITOR_TYPES.includes(monitor.type)) { + ctx.error(`${path}.type`, `Invalid type "${monitor.type}". Must be one of: ${MONITOR_TYPES.join(', ')}`) + } + + if (!monitor.config || typeof monitor.config !== 'object') { + ctx.error(`${path}.config`, '"config" is required and must be an object') + } else { + validateMonitorConfig(monitor.type, monitor.config, `${path}.config`, ctx) + } + + if (monitor.frequency !== undefined) { + validateFrequency(monitor.frequency, `${path}.frequency`, ctx) + } + + if (monitor.regions && !Array.isArray(monitor.regions)) { + ctx.error(`${path}.regions`, '"regions" must be an array of strings') + } + + if (monitor.environment) { + ctx.checkRef('environments', monitor.environment, `${path}.environment`) + } + if (monitor.tags) { + for (const name of monitor.tags) { + ctx.checkRef('tags', name, `${path}.tags`) + } + } + if (monitor.alertChannels) { + for (const name of monitor.alertChannels) { + ctx.checkRef('alertChannels', name, `${path}.alertChannels`) + } + } + if (monitor.resourceGroup) { + ctx.checkRef('resourceGroups', monitor.resourceGroup, `${path}.resourceGroup`) + } + + if (monitor.assertions) { + for (let i = 0; i < monitor.assertions.length; i++) { + validateAssertionDef(monitor.assertions[i], `${path}.assertions[${i}]`, ctx) + } + } + + if (monitor.auth) { + validateAuth(monitor.auth, `${path}.auth`, ctx) + } + + if (monitor.incidentPolicy) { + validateIncidentPolicy(monitor.incidentPolicy, `${path}.incidentPolicy`, ctx) + } +} + +function validateMonitorConfig(type: string, config: YamlMonitorConfig, path: string, ctx: ValidationContext): void { + switch (type) { + case 'HTTP': + if (!('url' in config) || !config.url) ctx.error(`${path}.url`, 'HTTP monitor requires "url"') + if ('method' in config && config.method && !HTTP_METHODS.includes(config.method as typeof HTTP_METHODS[number])) { + ctx.error(`${path}.method`, `Invalid HTTP method. Must be one of: ${HTTP_METHODS.join(', ')}`) + } + break + case 'DNS': + 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)) { + ctx.error(`${path}.recordTypes`, `Invalid DNS record type "${rt}". Must be one of: ${DNS_RECORD_TYPES.join(', ')}`) + } + } + } + break + case 'TCP': + if (!('host' in config) || !config.host) ctx.error(`${path}.host`, 'TCP monitor requires "host"') + if ('port' in config && config.port !== undefined && (typeof config.port !== 'number' || config.port < 1 || config.port > 65535)) { + ctx.error(`${path}.port`, 'TCP port must be between 1 and 65535') + } + break + case 'ICMP': + if (!('host' in config) || !config.host) ctx.error(`${path}.host`, 'ICMP monitor requires "host"') + break + case 'HEARTBEAT': + if (!('expectedInterval' in config) || typeof config.expectedInterval !== 'number' || config.expectedInterval <= 0) { + ctx.error(`${path}.expectedInterval`, 'Heartbeat monitor requires "expectedInterval" (positive number)') + } + if (!('gracePeriod' in config) || typeof config.gracePeriod !== 'number' || config.gracePeriod <= 0) { + ctx.error(`${path}.gracePeriod`, 'Heartbeat monitor requires "gracePeriod" (positive number)') + } + break + case 'MCP_SERVER': + if (!('command' in config) || !config.command) ctx.error(`${path}.command`, 'MCP_SERVER monitor requires "command"') + break + } +} + +function validateAssertionDef(assertion: YamlAssertion, path: string, ctx: ValidationContext): void { + if (!assertion.type) { + ctx.error(`${path}.type`, 'Assertion requires "type"') + } else if (!ASSERTION_TYPES.includes(assertion.type as typeof ASSERTION_TYPES[number])) { + ctx.error(`${path}.type`, `Unknown assertion type "${assertion.type}". See docs for valid assertion types.`) + } + + if (assertion.severity && !ASSERTION_SEVERITIES.includes(assertion.severity)) { + ctx.error(`${path}.severity`, `Assertion severity must be one of: ${ASSERTION_SEVERITIES.join(', ')}`) + } + + if (assertion.config && assertion.type) { + validateAssertionConfig(assertion.type, assertion.config, path, ctx) + } +} + +function validateAssertionConfig(type: string, config: Record, path: string, ctx: ValidationContext): void { + const needsOperator = ['StatusCodeAssertion', 'HeaderValueAssertion', 'JsonPathAssertion', 'RedirectTargetAssertion'] + if (needsOperator.includes(type)) { + if (config.operator && !COMPARISON_OPERATORS.includes(config.operator as string)) { + ctx.error(`${path}.config.operator`, `Invalid operator. Must be one of: ${COMPARISON_OPERATORS.join(', ')}`) + } + } +} + +function validateAuth(auth: YamlAuth, path: string, ctx: ValidationContext): void { + const validTypes = ['BearerAuthConfig', 'BasicAuthConfig', 'ApiKeyAuthConfig', 'HeaderAuthConfig'] + if (!auth.type || !validTypes.includes(auth.type)) { + ctx.error(`${path}.type`, `Auth type must be one of: ${validTypes.join(', ')}`) + } + if (!auth.secret) { + ctx.error(`${path}.secret`, 'Auth requires "secret" (vault secret key reference)') + } else { + ctx.checkRef('secrets', auth.secret, `${path}.secret`) + } + if ((auth.type === 'ApiKeyAuthConfig' || auth.type === 'HeaderAuthConfig') && !('headerName' in auth && auth.headerName)) { + ctx.error(`${path}.headerName`, `${auth.type} requires "headerName"`) + } +} + +function validateIncidentPolicy(policy: YamlIncidentPolicy, path: string, ctx: ValidationContext): void { + if (!policy.triggerRules || !Array.isArray(policy.triggerRules) || policy.triggerRules.length === 0) { + ctx.error(`${path}.triggerRules`, 'Incident policy requires at least one trigger rule') + return + } + + for (let i = 0; i < policy.triggerRules.length; i++) { + const rule = policy.triggerRules[i] + const rpath = `${path}.triggerRules[${i}]` + if (!TRIGGER_RULE_TYPES.includes(rule.type)) { + ctx.error(`${rpath}.type`, `Invalid trigger type. Must be one of: ${TRIGGER_RULE_TYPES.join(', ')}`) + } + if (rule.scope !== null && rule.scope !== undefined && !TRIGGER_SCOPES.includes(rule.scope)) { + ctx.error(`${rpath}.scope`, `Invalid scope. Must be one of: ${TRIGGER_SCOPES.join(', ')}`) + } + 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)) { + ctx.error(`${rpath}.aggregationType`, `Must be one of: ${TRIGGER_AGGREGATIONS.join(', ')}`) + } + } + + if (!policy.confirmation) { + ctx.error(`${path}.confirmation`, 'Incident policy requires "confirmation"') + } else if (policy.confirmation.type !== 'multi_region') { + ctx.error(`${path}.confirmation.type`, 'Confirmation type must be "multi_region"') + } + + if (!policy.recovery) { + ctx.error(`${path}.recovery`, 'Incident policy requires "recovery"') + } +} + +function validateFrequency(freq: number, path: string, ctx: ValidationContext): void { + if (typeof freq !== 'number') { + ctx.error(path, 'Frequency must be a number') + } else if (freq < MIN_FREQUENCY || freq > MAX_FREQUENCY) { + ctx.error(path, `Frequency must be between ${MIN_FREQUENCY} and ${MAX_FREQUENCY} seconds`) + } +} + +// ── Helpers ──────────────────────────────────────────────────────────── + +function validateDependency(dep: YamlDependency, path: string, ctx: ValidationContext): void { + requireString(dep, 'service', path, ctx) + if (dep.alertSensitivity && !ALERT_SENSITIVITIES.includes(dep.alertSensitivity as typeof ALERT_SENSITIVITIES[number])) { + ctx.error(`${path}.alertSensitivity`, `Must be one of: ${ALERT_SENSITIVITIES.join(', ')}`) + } +} + +function requireString(obj: object, field: string, path: string, ctx: ValidationContext): void { + const record = obj as Record + if (!record[field] || typeof record[field] !== 'string') { + ctx.error(`${path}.${field}`, `"${field}" is required`) + } +} diff --git a/test/fixtures/yaml/edge/all-channel-types.yml b/test/fixtures/yaml/edge/all-channel-types.yml new file mode 100644 index 0000000..64a76e3 --- /dev/null +++ b/test/fixtures/yaml/edge/all-channel-types.yml @@ -0,0 +1,42 @@ +alertChannels: + - name: slack-chan + type: slack + config: + webhookUrl: https://hooks.slack.com/services/T/B/x + mentionText: "@channel" + + - name: discord-chan + type: discord + config: + webhookUrl: https://discord.com/api/webhooks/123/abc + mentionRoleId: "12345" + + - name: email-chan + type: email + config: + recipients: [a@test.com, b@test.com] + + - name: pagerduty-chan + type: pagerduty + config: + routingKey: routing-key-123 + severityOverride: critical + + - name: opsgenie-chan + type: opsgenie + config: + apiKey: og-api-key-456 + region: us + + - name: teams-chan + type: teams + config: + webhookUrl: https://outlook.office.com/webhook/xxx + + - name: webhook-chan + type: webhook + config: + url: https://webhooks.example.com/alerts + signingSecret: secret-123 + customHeaders: + Authorization: Bearer token diff --git a/test/fixtures/yaml/edge/all-monitor-types.yml b/test/fixtures/yaml/edge/all-monitor-types.yml new file mode 100644 index 0000000..ac84074 --- /dev/null +++ b/test/fixtures/yaml/edge/all-monitor-types.yml @@ -0,0 +1,48 @@ +monitors: + - name: HTTP Monitor + type: HTTP + config: + url: https://example.com + method: POST + customHeaders: + X-Custom: value + requestBody: '{"health": true}' + contentType: application/json + verifyTls: true + + - name: DNS Monitor + type: DNS + config: + hostname: example.com + recordTypes: [A, AAAA, CNAME, MX, NS, TXT] + nameservers: ["8.8.8.8", "1.1.1.1"] + timeoutMs: 5000 + + - name: TCP Monitor + type: TCP + config: + host: db.example.com + port: 5432 + timeoutMs: 3000 + + - name: ICMP Monitor + type: ICMP + config: + host: gateway.example.com + packetCount: 5 + timeoutMs: 2000 + + - name: Heartbeat Monitor + type: HEARTBEAT + config: + expectedInterval: 60 + gracePeriod: 120 + + - name: MCP Monitor + type: MCP_SERVER + config: + command: node + args: ["server.js", "--port", "3000"] + env: + NODE_ENV: production + API_KEY: test-key diff --git a/test/fixtures/yaml/invalid/bad-channel-type.yml b/test/fixtures/yaml/invalid/bad-channel-type.yml new file mode 100644 index 0000000..52560d9 --- /dev/null +++ b/test/fixtures/yaml/invalid/bad-channel-type.yml @@ -0,0 +1,5 @@ +alertChannels: + - name: bad + type: sms + config: + phone: "+1234567890" diff --git a/test/fixtures/yaml/invalid/bad-escalation.yml b/test/fixtures/yaml/invalid/bad-escalation.yml new file mode 100644 index 0000000..1dc3407 --- /dev/null +++ b/test/fixtures/yaml/invalid/bad-escalation.yml @@ -0,0 +1,10 @@ +alertChannels: + - name: ops + type: slack + config: + webhookUrl: https://hooks.slack.com/test + +notificationPolicies: + - name: missing-steps + escalation: + steps: [] diff --git a/test/fixtures/yaml/invalid/bad-frequency.yml b/test/fixtures/yaml/invalid/bad-frequency.yml new file mode 100644 index 0000000..f38daf2 --- /dev/null +++ b/test/fixtures/yaml/invalid/bad-frequency.yml @@ -0,0 +1,7 @@ +monitors: + - name: Too Fast + type: HTTP + config: + url: https://example.com + method: GET + frequency: 5 diff --git a/test/fixtures/yaml/invalid/bad-type.yml b/test/fixtures/yaml/invalid/bad-type.yml new file mode 100644 index 0000000..188e89e --- /dev/null +++ b/test/fixtures/yaml/invalid/bad-type.yml @@ -0,0 +1,5 @@ +monitors: + - name: Bad Type + type: WEBSOCKET + config: + url: wss://example.com diff --git a/test/fixtures/yaml/invalid/duplicate-names.yml b/test/fixtures/yaml/invalid/duplicate-names.yml new file mode 100644 index 0000000..b614cd0 --- /dev/null +++ b/test/fixtures/yaml/invalid/duplicate-names.yml @@ -0,0 +1,11 @@ +monitors: + - name: Duplicate + type: HTTP + config: + url: https://a.example.com + method: GET + - name: Duplicate + type: HTTP + config: + url: https://b.example.com + method: GET diff --git a/test/fixtures/yaml/invalid/empty.yml b/test/fixtures/yaml/invalid/empty.yml new file mode 100644 index 0000000..b7f0ef8 --- /dev/null +++ b/test/fixtures/yaml/invalid/empty.yml @@ -0,0 +1,2 @@ +# Empty config — no resources defined +version: "1" diff --git a/test/fixtures/yaml/invalid/missing-env-var.yml b/test/fixtures/yaml/invalid/missing-env-var.yml new file mode 100644 index 0000000..563176d --- /dev/null +++ b/test/fixtures/yaml/invalid/missing-env-var.yml @@ -0,0 +1,10 @@ +monitors: + - name: Needs Token + type: HTTP + config: + url: https://example.com + method: GET + +secrets: + - key: api-key + value: ${MISSING_SECRET_VALUE} diff --git a/test/fixtures/yaml/invalid/missing-name.yml b/test/fixtures/yaml/invalid/missing-name.yml new file mode 100644 index 0000000..80d638b --- /dev/null +++ b/test/fixtures/yaml/invalid/missing-name.yml @@ -0,0 +1,5 @@ +monitors: + - type: HTTP + config: + url: https://example.com + method: GET diff --git a/test/fixtures/yaml/valid/defaults.yml b/test/fixtures/yaml/valid/defaults.yml new file mode 100644 index 0000000..bc31472 --- /dev/null +++ b/test/fixtures/yaml/valid/defaults.yml @@ -0,0 +1,20 @@ +defaults: + monitors: + frequency: 120 + enabled: true + regions: [us-east, eu-west] + +monitors: + - name: Monitor A + type: HTTP + config: + url: https://a.example.com + method: GET + + - name: Monitor B + type: HTTP + config: + url: https://b.example.com + method: GET + frequency: 30 + regions: [us-west] diff --git a/test/fixtures/yaml/valid/env-vars.yml b/test/fixtures/yaml/valid/env-vars.yml new file mode 100644 index 0000000..b235d02 --- /dev/null +++ b/test/fixtures/yaml/valid/env-vars.yml @@ -0,0 +1,12 @@ +monitors: + - name: Prod Monitor + type: HTTP + config: + url: ${APP_URL:-https://default.example.com} + method: GET + +alertChannels: + - name: slack-channel + type: slack + config: + webhookUrl: ${SLACK_WEBHOOK:-https://hooks.slack.com/default} diff --git a/test/fixtures/yaml/valid/full-stack.yml b/test/fixtures/yaml/valid/full-stack.yml new file mode 100644 index 0000000..782fb56 --- /dev/null +++ b/test/fixtures/yaml/valid/full-stack.yml @@ -0,0 +1,166 @@ +version: "1" + +defaults: + monitors: + frequency: 60 + enabled: true + regions: [us-east, eu-west] + +tags: + - name: production + color: "#EF4444" + - name: api + color: "#3B82F6" + +environments: + - name: Production + slug: production + isDefault: true + - name: Staging + slug: staging + +secrets: + - key: bearer-token + value: secret-value-123 + +alertChannels: + - name: ops-slack + type: slack + config: + webhookUrl: https://hooks.slack.com/services/T000/B000/xxx + - name: eng-email + type: email + config: + recipients: + - eng@company.com + - oncall@company.com + - name: pagerduty-critical + type: pagerduty + config: + routingKey: service-key-abc + +notificationPolicies: + - name: critical-escalation + enabled: true + priority: 1 + escalation: + steps: + - channels: [ops-slack] + delayMinutes: 0 + - channels: [pagerduty-critical] + delayMinutes: 5 + requireAck: true + +webhooks: + - url: https://hooks.company.com/devhelm + events: [monitor.down, monitor.recovered, incident.created] + description: Internal webhook for status page + +resourceGroups: + - name: API Services + description: Core API monitors + defaultFrequency: 30 + defaultRegions: [us-east, us-west, eu-west] + monitors: [API Health, DNS Check] + defaultAlertChannels: [ops-slack] + defaultEnvironment: production + healthThresholdType: PERCENTAGE + healthThresholdValue: 80 + +monitors: + - name: Website + type: HTTP + config: + url: https://www.company.com + method: GET + verifyTls: true + tags: [production] + alertChannels: [ops-slack] + environment: production + assertions: + - type: StatusCodeAssertion + config: + expected: "200" + operator: equals + severity: fail + - type: ResponseTimeAssertion + config: + thresholdMs: 2000 + severity: warn + - type: SslExpiryAssertion + config: + minDaysRemaining: 30 + severity: warn + + - name: API Health + type: HTTP + config: + url: https://api.company.com/health + method: GET + frequency: 30 + tags: [production, api] + alertChannels: [ops-slack, pagerduty-critical] + auth: + type: BearerAuthConfig + secret: bearer-token + incidentPolicy: + triggerRules: + - type: consecutive_failures + count: 3 + scope: per_region + severity: down + - type: response_time + thresholdMs: 5000 + scope: any_region + severity: degraded + aggregationType: p95 + confirmation: + type: multi_region + minRegionsFailing: 2 + maxWaitSeconds: 120 + recovery: + consecutiveSuccesses: 2 + minRegionsPassing: 2 + cooldownMinutes: 5 + + - name: DNS Check + type: DNS + config: + hostname: company.com + recordTypes: [A, AAAA, MX] + frequency: 300 + assertions: + - type: DnsResolvesAssertion + severity: fail + + - name: TCP Database + type: TCP + config: + host: db.internal.company.com + port: 5432 + frequency: 60 + + - name: ICMP Gateway + type: ICMP + config: + host: gateway.company.com + frequency: 60 + + - name: Worker Heartbeat + type: HEARTBEAT + config: + expectedInterval: 120 + gracePeriod: 300 + + - name: MCP Assistant + type: MCP_SERVER + config: + command: npx + args: ["-y", "@company/mcp-server"] + frequency: 300 + +dependencies: + - service: github + alertSensitivity: INCIDENTS_ONLY + - service: cloudflare + alertSensitivity: ALL diff --git a/test/fixtures/yaml/valid/minimal.yml b/test/fixtures/yaml/valid/minimal.yml new file mode 100644 index 0000000..cc7b7ea --- /dev/null +++ b/test/fixtures/yaml/valid/minimal.yml @@ -0,0 +1,6 @@ +monitors: + - name: Simple Health Check + type: HTTP + config: + url: https://example.com + method: GET diff --git a/test/fixtures/yaml/valid/multi-a.yml b/test/fixtures/yaml/valid/multi-a.yml new file mode 100644 index 0000000..85bd6ce --- /dev/null +++ b/test/fixtures/yaml/valid/multi-a.yml @@ -0,0 +1,11 @@ +tags: + - name: backend + color: "#6366F1" + +monitors: + - name: Backend API + type: HTTP + config: + url: https://api.example.com + method: GET + tags: [backend] diff --git a/test/fixtures/yaml/valid/multi-b.yml b/test/fixtures/yaml/valid/multi-b.yml new file mode 100644 index 0000000..87474b9 --- /dev/null +++ b/test/fixtures/yaml/valid/multi-b.yml @@ -0,0 +1,11 @@ +tags: + - name: frontend + color: "#10B981" + +monitors: + - name: Frontend App + type: HTTP + config: + url: https://app.example.com + method: GET + tags: [frontend] diff --git a/test/yaml/applier.test.ts b/test/yaml/applier.test.ts new file mode 100644 index 0000000..5999a84 --- /dev/null +++ b/test/yaml/applier.test.ts @@ -0,0 +1,668 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' +import {apply} from '../../src/lib/yaml/applier.js' +import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' +import type {Changeset, Change} from '../../src/lib/yaml/differ.js' + +vi.mock('../../src/lib/typed-api.js', () => ({ + typedGet: vi.fn(), + typedPost: vi.fn(), + typedPut: vi.fn(), + typedPatch: vi.fn(), + typedDelete: vi.fn(), + fetchPaginated: vi.fn(), +})) + +import {typedPost, typedPut, typedPatch, typedDelete} from '../../src/lib/typed-api.js' +const mockPost = vi.mocked(typedPost) +const mockPut = vi.mocked(typedPut) +const mockPatch = vi.mocked(typedPatch) +const mockDelete = vi.mocked(typedDelete) + +function emptyChangeset(): Changeset { + return {creates: [], updates: [], deletes: [], memberships: []} +} + +function emptyRefs(): ResolvedRefs { + return new ResolvedRefs() +} + +const fakeClient = {} as Parameters[2] + +describe('applier', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('creates', () => { + it('creates a tag', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'tag-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'tag', refKey: 'prod', desired: {name: 'prod', color: '#FF0000'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(result.succeeded[0].id).toBe('tag-new') + expect(result.failed).toHaveLength(0) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/tags', {name: 'prod', color: '#FF0000'}) + }) + + it('creates an environment', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'env-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'environment', refKey: 'staging', desired: {name: 'Staging', slug: 'staging'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/environments', expect.objectContaining({name: 'Staging', slug: 'staging'})) + }) + + it('creates a secret', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'sec-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'secret', refKey: 'api-key', desired: {key: 'api-key', value: 'secret123'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets', {key: 'api-key', value: 'secret123'}) + }) + + it('creates an alert channel', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'ch-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'alertChannel', refKey: 'slack', desired: {name: 'slack', type: 'slack', config: {webhookUrl: 'url'}}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/alert-channels', expect.objectContaining({name: 'slack'})) + }) + + it('creates a monitor', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'mon-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'monitor', refKey: 'API', desired: { + name: 'API', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}, + }}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(result.succeeded[0].id).toBe('mon-new') + expect(result.stateEntries).toHaveLength(1) + expect(result.stateEntries[0].resourceType).toBe('monitor') + expect(result.stateEntries[0].id).toBe('mon-new') + }) + + it('injects ref after create for downstream resolution', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'tag-99'}}) + const refs = emptyRefs() + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'tag', refKey: 'new-tag', desired: {name: 'new-tag'}}], + } + await apply(changeset, refs, fakeClient) + expect(refs.resolve('tags', 'new-tag')).toBe('tag-99') + }) + + it('creates a notification policy with channel refs resolved in escalation', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'np-new'}}) + const refs = emptyRefs() + refs.set('alertChannels', 'slack', {id: 'ch-1', refKey: 'slack', raw: {}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{ + action: 'create', resourceType: 'notificationPolicy', refKey: 'default', + desired: { + name: 'default', + escalation: {steps: [{channels: ['slack'], delayMinutes: 0}]}, + }, + }], + } + const result = await apply(changeset, refs, fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith( + fakeClient, + '/api/v1/notification-policies', + expect.objectContaining({ + name: 'default', + escalation: expect.objectContaining({ + steps: [expect.objectContaining({channelIds: ['ch-1']})], + }), + }), + ) + }) + + it('creates a webhook', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'wh-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{ + action: 'create', resourceType: 'webhook', refKey: 'hook1', + desired: {url: 'https://hook.com', events: ['monitor.down']}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks', { + url: 'https://hook.com', + subscribedEvents: ['monitor.down'], + description: undefined, + }) + }) + + it('creates a resource group', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'rg-new'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'resourceGroup', refKey: 'API', desired: {name: 'API Group'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith( + fakeClient, + '/api/v1/resource-groups', + expect.objectContaining({name: 'API Group'}), + ) + }) + + it('creates a dependency (service subscription)', async () => { + mockPost.mockResolvedValueOnce({data: {subscriptionId: 'sub-gh'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{ + action: 'create', resourceType: 'dependency', refKey: 'github', + desired: {service: 'github', alertSensitivity: 'ALL'}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/github', { + alertSensitivity: 'ALL', + componentId: null, + }) + }) + }) + + describe('updates', () => { + it('updates a tag via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'tag', refKey: 'prod', existingId: 'tag-1', + desired: {name: 'prod', color: '#00FF00'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/tags/tag-1', {name: 'prod', color: '#00FF00'}) + }) + + it('updates a monitor via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'monitor', refKey: 'API', existingId: 'mon-1', + desired: {name: 'API', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}, frequency: 30}, + current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(result.stateEntries).toHaveLength(1) + expect(result.stateEntries[0].id).toBe('mon-1') + }) + + it('updates a dependency via PATCH', async () => { + mockPatch.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'dependency', refKey: 'github', existingId: 'dep-1', + desired: {service: 'github', alertSensitivity: 'ALL'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPatch).toHaveBeenCalledWith( + fakeClient, '/api/v1/service-subscriptions/dep-1/alert-sensitivity', + {alertSensitivity: 'ALL'}, + ) + }) + + it('updates an environment via PUT using environment id in path', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'environment', refKey: 'prod', existingId: 'env-42', + desired: {name: 'Prod', slug: 'prod', variables: {KEY: 'val'}}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/environments/env-42', { + name: 'Prod', + variables: {KEY: 'val'}, + isDefault: undefined, + }) + }) + + it('updates a secret via PUT keyed by secret key', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'secret', refKey: 'k', existingId: 'sec-1', + desired: {key: 'k', value: 'newval'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets/k', {value: 'newval'}) + }) + + it('updates an alert channel via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'alertChannel', refKey: 'slack', existingId: 'ch-1', + desired: {name: 'slack', type: 'slack', config: {webhookUrl: 'url'}}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith( + fakeClient, + '/api/v1/alert-channels/ch-1', + expect.objectContaining({ + name: 'slack', + config: expect.objectContaining({channelType: 'SlackChannelConfig', webhookUrl: 'url'}), + }), + ) + }) + + it('updates a notification policy via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const refs = emptyRefs() + refs.set('alertChannels', 'slack', {id: 'ch-1', refKey: 'slack', raw: {}}) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'notificationPolicy', refKey: 'pol', existingId: 'np-1', + desired: { + name: 'pol', + escalation: {steps: [{channels: ['slack']}]}, + }, + current: {}, + }], + } + const result = await apply(changeset, refs, fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith( + fakeClient, + '/api/v1/notification-policies/np-1', + expect.objectContaining({ + name: 'pol', + escalation: expect.objectContaining({ + steps: [expect.objectContaining({channelIds: ['ch-1']})], + }), + }), + ) + }) + + it('updates a webhook via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'webhook', refKey: 'w', existingId: 'wh-1', + desired: {url: 'https://hook.com', events: ['monitor.up']}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks/wh-1', { + url: 'https://hook.com', + subscribedEvents: ['monitor.up'], + description: undefined, + }) + }) + + it('updates a resource group via PUT', async () => { + mockPut.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'resourceGroup', refKey: 'G', existingId: 'rg-9', + desired: {name: 'Renamed'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPut).toHaveBeenCalledWith( + fakeClient, + '/api/v1/resource-groups/rg-9', + expect.objectContaining({name: 'Renamed'}), + ) + }) + + it('updates dependency component via PATCH to subscription', async () => { + mockPatch.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'dependency', refKey: 'gh', existingId: 'dep-2', + desired: {service: 'gh', component: 'api'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPatch).toHaveBeenCalledTimes(1) + expect(mockPatch).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/dep-2', { + componentId: 'api', + }) + }) + + it('does not PATCH alert-sensitivity when dependency update omits alertSensitivity', async () => { + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{ + action: 'update', resourceType: 'dependency', refKey: 'gh', existingId: 'dep-3', + desired: {service: 'gh'}, current: {}, + }], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPatch).not.toHaveBeenCalled() + }) + }) + + describe('deletes', () => { + it('deletes a tag', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'tag', refKey: 'old', existingId: 'tag-1', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/tags/tag-1') + }) + + it('deletes a monitor', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'monitor', refKey: 'old', existingId: 'mon-1', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/monitors/mon-1') + }) + + it('deletes an environment', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'environment', refKey: 'stg', existingId: 'env-7', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/environments/env-7') + }) + + it('deletes a secret', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'secret', refKey: 'k', existingId: 'sec-x', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets/sec-x') + }) + + it('deletes an alert channel', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'alertChannel', refKey: 's', existingId: 'ch-9', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/alert-channels/ch-9') + }) + + it('deletes a notification policy', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'notificationPolicy', refKey: 'p', existingId: 'np-2', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/notification-policies/np-2') + }) + + it('deletes a webhook', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'webhook', refKey: 'w', existingId: 'wh-3', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks/wh-3') + }) + + it('deletes a resource group', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'resourceGroup', refKey: 'G', existingId: 'rg-4', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-4') + }) + + it('deletes a dependency', async () => { + mockDelete.mockResolvedValueOnce(undefined) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'dependency', refKey: 'gh', existingId: 'dep-z', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/dep-z') + }) + }) + + describe('memberships', () => { + it('creates group membership', async () => { + mockPost.mockResolvedValueOnce(undefined) + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'API', {id: 'rg-1', refKey: 'API', raw: {}}) + refs.set('monitors', 'Health', {id: 'mon-1', refKey: 'Health', raw: {}}) + const changeset: Changeset = { + ...emptyChangeset(), + memberships: [{ + action: 'create', resourceType: 'groupMembership', refKey: 'API → Health', + desired: {groupName: 'API', memberType: 'monitor', memberRef: 'Health'}, + }], + } + const result = await apply(changeset, refs, fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-1/members', { + memberType: 'monitor', memberId: 'mon-1', + }) + }) + + it('creates service membership', async () => { + mockPost.mockResolvedValueOnce(undefined) + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: {}}) + refs.set('dependencies', 'github', {id: 'dep-1', refKey: 'github', raw: {}}) + const changeset: Changeset = { + ...emptyChangeset(), + memberships: [{ + action: 'create', resourceType: 'groupMembership', refKey: 'G → github', + desired: {groupName: 'G', memberType: 'service', memberRef: 'github'}, + }], + } + const result = await apply(changeset, refs, fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-1/members', { + memberType: 'service', memberId: 'dep-1', + }) + }) + }) + + describe('error handling', () => { + it('continues after create failure', async () => { + mockPost + .mockRejectedValueOnce(new Error('API error on first')) + .mockResolvedValueOnce({data: {id: 'tag-2'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [ + {action: 'create', resourceType: 'tag', refKey: 'fail', desired: {name: 'fail'}}, + {action: 'create', resourceType: 'tag', refKey: 'ok', desired: {name: 'ok'}}, + ], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(1) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].refKey).toBe('fail') + expect(result.failed[0].error).toContain('API error on first') + expect(result.succeeded[0].refKey).toBe('ok') + }) + + it('continues after update failure', async () => { + mockPut.mockRejectedValueOnce(new Error('update failed')) + const changeset: Changeset = { + ...emptyChangeset(), + updates: [{action: 'update', resourceType: 'tag', refKey: 'T', existingId: 't-1', desired: {name: 'T'}, current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].error).toContain('update failed') + }) + + it('continues after delete failure', async () => { + mockDelete.mockRejectedValueOnce(new Error('delete failed')) + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'monitor', refKey: 'M', existingId: 'm-1', current: {}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.failed).toHaveLength(1) + }) + + it('continues after membership failure', async () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: {}}) + mockPost.mockRejectedValueOnce(new Error('membership failed')) + const changeset: Changeset = { + ...emptyChangeset(), + memberships: [{ + action: 'create', resourceType: 'groupMembership', refKey: 'G → M', + desired: {groupName: 'G', memberType: 'monitor', memberRef: 'M'}, + }], + } + const result = await apply(changeset, refs, fakeClient) + expect(result.failed).toHaveLength(1) + }) + + it('fails create when API returns no extractable id', async () => { + mockPost.mockResolvedValueOnce({data: {}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'tag', refKey: 'T', desired: {name: 'T'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(0) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].error).toBe('Create succeeded but API returned no resource ID') + }) + + it('records unknown resourceType on create as failure', async () => { + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'bogus', refKey: 'x', desired: {}} as Change], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].error).toContain('Unknown resource type for create') + }) + + it('records unknown resourceType on delete as failure', async () => { + const changeset: Changeset = { + ...emptyChangeset(), + deletes: [{action: 'delete', resourceType: 'bogus', refKey: 'x', existingId: '1', current: {}} as Change], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].error).toContain('Unknown resource type for delete') + }) + + it('string rejection becomes error message via String()', async () => { + mockPost.mockRejectedValueOnce('plain string failure') + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'tag', refKey: 'T', desired: {name: 'T'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.failed).toHaveLength(1) + expect(result.failed[0].error).toBe('plain string failure') + }) + }) + + describe('typed response extraction', () => { + it('extracts id from typed tag response', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'wrapped-id'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'tag', refKey: 'T', desired: {name: 'T'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded[0].id).toBe('wrapped-id') + }) + + it('extracts subscriptionId from dependency response', async () => { + mockPost.mockResolvedValueOnce({data: {subscriptionId: 'sub-123'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'dependency', refKey: 'gh', desired: {service: 'gh'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded[0].id).toBe('sub-123') + }) + + it('extracts id from secret response', async () => { + mockPost.mockResolvedValueOnce({data: {id: 'sec-uuid'}}) + const changeset: Changeset = { + ...emptyChangeset(), + creates: [{action: 'create', resourceType: 'secret', refKey: 'my-key', desired: {key: 'my-key', value: 'v'}}], + } + const result = await apply(changeset, emptyRefs(), fakeClient) + expect(result.succeeded[0].id).toBe('sec-uuid') + }) + }) + + describe('empty changeset', () => { + it('returns empty result for no-op changeset', async () => { + const result = await apply(emptyChangeset(), emptyRefs(), fakeClient) + expect(result.succeeded).toHaveLength(0) + expect(result.failed).toHaveLength(0) + expect(result.stateEntries).toHaveLength(0) + }) + }) +}) diff --git a/test/yaml/differ.test.ts b/test/yaml/differ.test.ts new file mode 100644 index 0000000..db5083d --- /dev/null +++ b/test/yaml/differ.test.ts @@ -0,0 +1,949 @@ +import {describe, it, expect} from 'vitest' +import {diff, formatPlan} from '../../src/lib/yaml/differ.js' +import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' +import type {DevhelmConfig} from '../../src/lib/yaml/schema.js' + +function emptyRefs(): ResolvedRefs { + return new ResolvedRefs() +} + +describe('differ', () => { + describe('diff', () => { + it('detects creates for new resources', () => { + const config: DevhelmConfig = { + tags: [{name: 'production', color: '#EF4444'}], + monitors: [{ + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, emptyRefs()) + expect(changeset.creates).toHaveLength(2) + expect(changeset.creates[0].resourceType).toBe('tag') + expect(changeset.creates[1].resourceType).toBe('monitor') + }) + + it('detects updates when field changed', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'production', {id: 'tag-1', refKey: 'production', raw: {name: 'production', color: '#000000'}}) + const config: DevhelmConfig = { + tags: [{name: 'production', color: '#EF4444'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + expect(changeset.updates[0].existingId).toBe('tag-1') + expect(changeset.creates).toHaveLength(0) + }) + + it('skips update when tag unchanged', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'production', {id: 'tag-1', refKey: 'production', raw: {name: 'production', color: '#EF4444'}}) + const config: DevhelmConfig = { + tags: [{name: 'production', color: '#EF4444'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('skips update when monitor unchanged', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'API', {id: 'mon-1', refKey: 'API', raw: { + name: 'API', type: 'HTTP', enabled: true, frequencySeconds: 60, + regions: ['us-east', 'eu-west'], + config: {url: 'https://api.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'API', type: 'HTTP', enabled: true, frequency: 60, + regions: ['us-east', 'eu-west'], + config: {url: 'https://api.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects update when monitor frequency changed', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'API', {id: 'mon-1', refKey: 'API', raw: { + name: 'API', type: 'HTTP', frequencySeconds: 60, + config: {url: 'https://api.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'API', type: 'HTTP', frequency: 30, + config: {url: 'https://api.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when monitor config changed', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'API', {id: 'mon-1', refKey: 'API', raw: { + name: 'API', type: 'HTTP', + config: {url: 'https://api.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'API', type: 'HTTP', + config: {url: 'https://api.com/v2', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when monitor regions changed', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'API', {id: 'mon-1', refKey: 'API', raw: { + name: 'API', type: 'HTTP', regions: ['us-east'], + config: {url: 'https://api.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'API', type: 'HTTP', regions: ['us-east', 'eu-west'], + config: {url: 'https://api.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when webhook unchanged', () => { + const refs = new ResolvedRefs() + refs.set('webhooks', 'https://hooks.com/x', {id: 'wh-1', refKey: 'https://hooks.com/x', raw: { + url: 'https://hooks.com/x', subscribedEvents: ['monitor.down', 'monitor.recovered'], description: 'test', + }}) + const config: DevhelmConfig = { + webhooks: [{url: 'https://hooks.com/x', events: ['monitor.down', 'monitor.recovered'], description: 'test'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects update when webhook events changed', () => { + const refs = new ResolvedRefs() + refs.set('webhooks', 'https://hooks.com/x', {id: 'wh-1', refKey: 'https://hooks.com/x', raw: { + url: 'https://hooks.com/x', subscribedEvents: ['monitor.down'], + }}) + const config: DevhelmConfig = { + webhooks: [{url: 'https://hooks.com/x', events: ['monitor.down', 'incident.created']}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when environment unchanged', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'production', {id: 'env-1', refKey: 'production', raw: { + name: 'Production', slug: 'production', isDefault: true, + }}) + const config: DevhelmConfig = { + environments: [{name: 'Production', slug: 'production', isDefault: true}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('always updates secrets (value not visible in API)', () => { + const refs = new ResolvedRefs() + refs.set('secrets', 'api-key', {id: 'sec-1', refKey: 'api-key', raw: {key: 'api-key'}}) + const config: DevhelmConfig = { + secrets: [{key: 'api-key', value: 'same-or-different'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when dependency unchanged', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'github', {id: 'dep-1', refKey: 'github', raw: { + slug: 'github', alertSensitivity: 'INCIDENTS_ONLY', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'github', alertSensitivity: 'INCIDENTS_ONLY'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects update when dependency alertSensitivity changed', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'github', {id: 'dep-1', refKey: 'github', raw: { + slug: 'github', alertSensitivity: 'INCIDENTS_ONLY', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'github', alertSensitivity: 'ALL'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when resource group unchanged', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'API', {id: 'rg-1', refKey: 'API', raw: { + name: 'API', description: 'API services', defaultFrequency: 30, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'API', description: 'API services', defaultFrequency: 30}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects no changes for empty config sections', () => { + const config: DevhelmConfig = {} + const changeset = diff(config, emptyRefs()) + expect(changeset.creates).toHaveLength(0) + expect(changeset.updates).toHaveLength(0) + expect(changeset.deletes).toHaveLength(0) + }) + + it('detects deletes with prune=true', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'old-monitor', { + id: 'mon-1', refKey: 'old-monitor', managedBy: 'CLI', raw: {managedBy: 'CLI'}, + }) + const config: DevhelmConfig = {monitors: []} + const changeset = diff(config, refs, {prune: true}) + expect(changeset.deletes).toHaveLength(1) + expect(changeset.deletes[0].refKey).toBe('old-monitor') + }) + + it('skips non-CLI monitors during prune', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'dashboard-monitor', { + id: 'mon-1', refKey: 'dashboard-monitor', managedBy: 'DASHBOARD', raw: {managedBy: 'DASHBOARD'}, + }) + const config: DevhelmConfig = {monitors: []} + const changeset = diff(config, refs, {prune: true}) + expect(changeset.deletes).toHaveLength(0) + }) + + it('prune: omitted section (undefined) does not delete', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'T', {id: '1', refKey: 'T', raw: {name: 'T'}}) + const config: DevhelmConfig = {} + const changeset = diff(config, refs, {prune: true}) + expect(changeset.deletes).toHaveLength(0) + }) + + it('prune: empty array deletes all existing', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'T', {id: '1', refKey: 'T', raw: {name: 'T'}}) + const config: DevhelmConfig = {tags: []} + const changeset = diff(config, refs, {prune: true}) + expect(changeset.deletes).toHaveLength(1) + }) + + it('does not delete without prune flag', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'old', {id: 'mon-1', refKey: 'old', managedBy: 'CLI', raw: {}}) + const config: DevhelmConfig = {monitors: []} + const changeset = diff(config, refs, {prune: false}) + expect(changeset.deletes).toHaveLength(0) + }) + + it('creates in dependency order', () => { + const config: DevhelmConfig = { + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + tags: [{name: 'T'}], + alertChannels: [{name: 'C', type: 'slack', config: {webhookUrl: 'url'}}], + } + const changeset = diff(config, emptyRefs()) + const types = changeset.creates.map((c) => c.resourceType) + expect(types.indexOf('tag')).toBeLessThan(types.indexOf('alertChannel')) + expect(types.indexOf('alertChannel')).toBeLessThan(types.indexOf('monitor')) + }) + + it('deletes in reverse dependency order', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'T', {id: '1', refKey: 'T', raw: {}}) + refs.set('monitors', 'M', {id: '2', refKey: 'M', managedBy: 'CLI', raw: {}}) + const config: DevhelmConfig = {tags: [], monitors: []} + const changeset = diff(config, refs, {prune: true}) + const types = changeset.deletes.map((c) => c.resourceType) + expect(types.indexOf('monitor')).toBeLessThan(types.indexOf('tag')) + }) + + it('detects group memberships', () => { + const config: DevhelmConfig = { + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + resourceGroups: [{name: 'G', monitors: ['M']}], + } + const changeset = diff(config, emptyRefs()) + expect(changeset.memberships).toHaveLength(1) + expect(changeset.memberships[0].refKey).toBe('G → M') + }) + + it('handles all resource types', () => { + const config: DevhelmConfig = { + tags: [{name: 'T'}], + environments: [{name: 'E', slug: 'e'}], + secrets: [{key: 'K', value: 'V'}], + alertChannels: [{name: 'C', type: 'slack', config: {webhookUrl: 'url'}}], + notificationPolicies: [{name: 'P', escalation: {steps: [{channels: ['C']}]}}], + webhooks: [{url: 'https://x.com', events: ['e']}], + resourceGroups: [{name: 'G'}], + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + dependencies: [{service: 'github'}], + } + const changeset = diff(config, emptyRefs()) + expect(changeset.creates).toHaveLength(9) + }) + + it('monitor regions order does not matter', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'API', {id: 'mon-1', refKey: 'API', raw: { + name: 'API', type: 'HTTP', regions: ['eu-west', 'us-east'], + config: {url: 'https://api.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'API', type: 'HTTP', regions: ['us-east', 'eu-west'], + config: {url: 'https://api.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('webhook events order does not matter', () => { + const refs = new ResolvedRefs() + refs.set('webhooks', 'https://x.com', {id: 'wh-1', refKey: 'https://x.com', raw: { + url: 'https://x.com', subscribedEvents: ['b', 'a'], + }}) + const config: DevhelmConfig = { + webhooks: [{url: 'https://x.com', events: ['a', 'b']}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('always updates alert channels (discriminated union config)', () => { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'slack-ops', {id: 'ch-1', refKey: 'slack-ops', raw: { + id: 'ch-1', name: 'slack-ops', channelType: 'slack', + displayConfig: {}, createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', + }}) + const config: DevhelmConfig = { + alertChannels: [{name: 'slack-ops', type: 'slack', config: {webhookUrl: 'https://hooks.slack.com/test'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when notification policy escalation changes', () => { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'ch', {id: 'ch-1', refKey: 'ch', raw: {name: 'ch'}}) + refs.set('notificationPolicies', 'critical', {id: 'np-1', refKey: 'critical', raw: { + name: 'critical', enabled: true, priority: 0, + escalation: {steps: [{channelIds: ['ch-old'], delayMinutes: 0}]}, + }}) + const config: DevhelmConfig = { + notificationPolicies: [{name: 'critical', enabled: true, priority: 0, escalation: {steps: [{channels: ['ch']}]}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('mixed: only changed resources appear as updates', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'unchanged', {id: 'tag-1', refKey: 'unchanged', raw: {name: 'unchanged', color: '#FF0000'}}) + refs.set('tags', 'changed', {id: 'tag-2', refKey: 'changed', raw: {name: 'changed', color: '#000000'}}) + refs.set('webhooks', 'https://same.com', {id: 'wh-1', refKey: 'https://same.com', raw: { + url: 'https://same.com', subscribedEvents: ['a'], description: 'same', + }}) + const config: DevhelmConfig = { + tags: [ + {name: 'unchanged', color: '#FF0000'}, + {name: 'changed', color: '#00FF00'}, + {name: 'brand-new'}, + ], + webhooks: [{url: 'https://same.com', events: ['a'], description: 'same'}], + } + const changeset = diff(config, refs) + expect(changeset.creates).toHaveLength(1) + expect(changeset.creates[0].refKey).toBe('brand-new') + expect(changeset.updates).toHaveLength(1) + expect(changeset.updates[0].refKey).toBe('changed') + }) + + it('detects update when monitor enabled toggled', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', enabled: true, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', enabled: false, + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when environment variables change', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'prod', {id: 'env-1', refKey: 'prod', raw: { + name: 'Prod', slug: 'prod', variables: {A: '1'}, + }}) + const config: DevhelmConfig = { + environments: [{name: 'Prod', slug: 'prod', variables: {A: '1', B: '2'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when environment variables same', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'prod', {id: 'env-1', refKey: 'prod', raw: { + name: 'Prod', slug: 'prod', variables: {A: '1', B: '2'}, + }}) + const config: DevhelmConfig = { + environments: [{name: 'Prod', slug: 'prod', variables: {B: '2', A: '1'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects update when resource group health threshold changes', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', healthThresholdType: 'PERCENTAGE', healthThresholdValue: 80, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', healthThresholdType: 'PERCENTAGE', healthThresholdValue: 50}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when resource group suppressMemberAlerts changes', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', suppressMemberAlerts: false, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', suppressMemberAlerts: true}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('tag without color ignores API color (undefined not compared)', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'T', {id: 'tag-1', refKey: 'T', raw: {name: 'T', color: '#FF0000'}}) + const config: DevhelmConfig = { + tags: [{name: 'T'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('config key ordering does not trigger false update', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + config: {method: 'GET', url: 'https://api.com', headers: {a: '1', b: '2'}}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', + config: {url: 'https://api.com', method: 'GET', headers: {b: '2', a: '1'}}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('monitor without frequency set ignores API frequencySeconds', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', frequencySeconds: 60, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('monitor without regions set ignores API regions', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', regions: ['us-east', 'eu-west'], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Monitor tags: YAML uses names, API returns TagDto[] ────────── + + it('detects update when monitor tags change', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'a', {id: 'tag-a', refKey: 'a', raw: {id: 'tag-a', name: 'a'}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + tags: [{id: 'tag-a', name: 'a'}], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', tags: ['a', 'b'], + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when monitor tags unchanged', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'a', {id: 'tag-a', refKey: 'a', raw: {id: 'tag-a', name: 'a'}}) + refs.set('tags', 'b', {id: 'tag-b', refKey: 'b', raw: {id: 'tag-b', name: 'b'}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + tags: [{id: 'tag-a', name: 'a'}, {id: 'tag-b', name: 'b'}], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', tags: ['b', 'a'], + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('monitor without tags set ignores API tags', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + tags: [{id: 'tag-x', name: 'x'}], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{name: 'M', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Monitor alertChannels: YAML names → API alertChannelIds (UUIDs) ── + + it('detects update when monitor alertChannels change', () => { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'ch1', {id: 'ch-uuid-1', refKey: 'ch1', raw: {id: 'ch-uuid-1', name: 'ch1'}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', alertChannelIds: ['ch-uuid-1'], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', alertChannels: ['ch1', 'ch2'], + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when monitor alertChannels unchanged', () => { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'ch1', {id: 'ch-uuid-1', refKey: 'ch1', raw: {}}) + refs.set('alertChannels', 'ch2', {id: 'ch-uuid-2', refKey: 'ch2', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', alertChannelIds: ['ch-uuid-1', 'ch-uuid-2'], + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', alertChannels: ['ch2', 'ch1'], + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Monitor environment: YAML slug → API Summary { id, name, slug } ── + + it('detects update when monitor environment changes', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'prod', {id: 'env-prod', refKey: 'prod', raw: {}}) + refs.set('environments', 'staging', {id: 'env-stg', refKey: 'staging', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + environment: {id: 'env-prod', name: 'Production', slug: 'prod'}, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', environment: 'staging', + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when monitor environment unchanged', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'prod', {id: 'env-prod', refKey: 'prod', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + environment: {id: 'env-prod', name: 'Production', slug: 'prod'}, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', environment: 'prod', + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Monitor auth: YAML uses secret name, API uses MonitorAuthDto ── + + it('detects update when monitor auth type changes', () => { + const refs = new ResolvedRefs() + refs.set('secrets', 'creds', {id: 'sec-1', refKey: 'creds', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + auth: {authType: 'bearer', config: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}}, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', + auth: {type: 'BasicAuthConfig', secret: 'creds'}, + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when monitor auth unchanged', () => { + const refs = new ResolvedRefs() + refs.set('secrets', 'token', {id: 'sec-1', refKey: 'token', raw: {}}) + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + auth: {authType: 'bearer', config: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}}, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', + auth: {type: 'BearerAuthConfig', secret: 'token'}, + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Monitor incidentPolicy ────────────────────────────────────── + + it('detects update when monitor incidentPolicy changes', () => { + const apiPolicy = { + triggerRules: [{ + type: 'consecutive_failures', count: 2, scope: 'per_region', severity: 'down', + }], + confirmation: {type: 'multi_region', minRegionsFailing: 1}, + recovery: {consecutiveSuccesses: 2}, + } + const yamlPolicy = { + triggerRules: [{ + type: 'consecutive_failures' as const, count: 3, scope: 'per_region' as const, severity: 'down' as const, + }], + confirmation: {type: 'multi_region' as const, minRegionsFailing: 1}, + recovery: {consecutiveSuccesses: 2}, + } + const refs = new ResolvedRefs() + refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { + name: 'M', type: 'HTTP', + incidentPolicy: {id: 'ip-1', monitorId: 'mon-1', ...apiPolicy}, + config: {url: 'https://x.com', method: 'GET'}, + }}) + const config: DevhelmConfig = { + monitors: [{ + name: 'M', type: 'HTTP', incidentPolicy: yamlPolicy, + config: {url: 'https://x.com', method: 'GET'}, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + // ── Resource group: YAML names → API UUIDs ───────────────────── + + it('detects update when resource group alertPolicy changes', () => { + const refs = new ResolvedRefs() + refs.set('notificationPolicies', 'old', {id: 'np-old-uuid', refKey: 'old', raw: {}}) + refs.set('notificationPolicies', 'new', {id: 'np-new-uuid', refKey: 'new', raw: {}}) + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', alertPolicyId: 'np-old-uuid', + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', alertPolicy: 'new'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when resource group defaultRegions change', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', defaultRegions: ['us-east'], + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', defaultRegions: ['us-east', 'eu-west']}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when resource group defaultRetryStrategy changes', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', defaultRetryStrategy: {type: 'fixed', maxRetries: 2, interval: 5}, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', defaultRetryStrategy: {type: 'fixed', maxRetries: 3, interval: 5}}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when resource group confirmationDelaySeconds changes', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', confirmationDelaySeconds: 30, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', confirmationDelaySeconds: 60}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('detects update when resource group recoveryCooldownMinutes changes', () => { + const refs = new ResolvedRefs() + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', recoveryCooldownMinutes: 10, + }}) + const config: DevhelmConfig = { + resourceGroups: [{name: 'G', recoveryCooldownMinutes: 20}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when all resource group fields match (with resolved refs)', () => { + const refs = new ResolvedRefs() + refs.set('notificationPolicies', 'critical', {id: 'np-uuid-1', refKey: 'critical', raw: {}}) + refs.set('alertChannels', 'ch1', {id: 'ch-uuid-1', refKey: 'ch1', raw: {}}) + refs.set('alertChannels', 'ch2', {id: 'ch-uuid-2', refKey: 'ch2', raw: {}}) + refs.set('environments', 'prod', {id: 'env-uuid-1', refKey: 'prod', raw: {}}) + refs.set('resourceGroups', 'G', {id: 'rg-1', refKey: 'G', raw: { + name: 'G', + description: 'desc', + alertPolicyId: 'np-uuid-1', + defaultFrequency: 30, + defaultRegions: ['us-east', 'eu-west'], + defaultRetryStrategy: {type: 'fixed', maxRetries: 3, interval: 10}, + defaultAlertChannels: ['ch-uuid-1', 'ch-uuid-2'], + defaultEnvironmentId: 'env-uuid-1', + confirmationDelaySeconds: 60, + recoveryCooldownMinutes: 120, + healthThresholdType: 'PERCENTAGE', + healthThresholdValue: 80, + suppressMemberAlerts: true, + }}) + const config: DevhelmConfig = { + resourceGroups: [{ + name: 'G', + description: 'desc', + alertPolicy: 'critical', + defaultFrequency: 30, + defaultRegions: ['eu-west', 'us-east'], + defaultRetryStrategy: {type: 'fixed', maxRetries: 3, interval: 10}, + defaultAlertChannels: ['ch2', 'ch1'], + defaultEnvironment: 'prod', + confirmationDelaySeconds: 60, + recoveryCooldownMinutes: 120, + healthThresholdType: 'PERCENTAGE', + healthThresholdValue: 80, + suppressMemberAlerts: true, + }], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('detects update when webhook API has no subscribedEvents but YAML has events', () => { + const refs = new ResolvedRefs() + refs.set('webhooks', 'https://hooks.com/nonevents', {id: 'wh-1', refKey: 'https://hooks.com/nonevents', raw: { + url: 'https://hooks.com/nonevents', + }}) + const config: DevhelmConfig = { + webhooks: [{url: 'https://hooks.com/nonevents', events: ['e']}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('dependency without alertSensitivity is unchanged', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'gh', {id: 'dep-1', refKey: 'gh', raw: { + slug: 'gh', alertSensitivity: 'ALL', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'gh'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + // ── Dependency component: YAML uses componentId UUID, API has componentId ── + + it('detects update when dependency component changes', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'gh', {id: 'dep-1', refKey: 'gh', raw: { + slug: 'gh', componentId: 'comp-api', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'gh', component: 'comp-actions'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(1) + }) + + it('skips update when dependency component unchanged', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'gh', {id: 'dep-1', refKey: 'gh', raw: { + slug: 'gh', componentId: 'comp-api', alertSensitivity: 'ALL', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'gh', component: 'comp-api', alertSensitivity: 'ALL'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + + it('dependency without component set ignores API componentId', () => { + const refs = new ResolvedRefs() + refs.set('dependencies', 'gh', {id: 'dep-1', refKey: 'gh', raw: { + slug: 'gh', componentId: 'comp-actions', + }}) + const config: DevhelmConfig = { + dependencies: [{service: 'gh'}], + } + const changeset = diff(config, refs) + expect(changeset.updates).toHaveLength(0) + }) + }) + + describe('formatPlan', () => { + it('shows no changes message', () => { + const result = formatPlan({creates: [], updates: [], deletes: [], memberships: []}) + expect(result).toContain('No changes') + }) + + it('shows create/update/delete counts', () => { + const changeset = { + creates: [{action: 'create' as const, resourceType: 'monitor' as const, refKey: 'M'}], + updates: [{action: 'update' as const, resourceType: 'tag' as const, refKey: 'T', existingId: '1'}], + deletes: [{action: 'delete' as const, resourceType: 'tag' as const, refKey: 'X', existingId: '2'}], + memberships: [], + } + const result = formatPlan(changeset) + expect(result).toContain('1 to create') + expect(result).toContain('1 to update') + expect(result).toContain('1 to delete') + expect(result).toContain('+ monitor "M"') + expect(result).toContain('~ tag "T"') + expect(result).toContain('- tag "X"') + }) + + it('shows memberships', () => { + const changeset = { + creates: [], + updates: [], + deletes: [], + memberships: [{action: 'create' as const, resourceType: 'groupMembership' as const, refKey: 'API → Health Check'}], + } + const result = formatPlan(changeset) + expect(result).toContain('1 memberships') + expect(result).toContain('→ API → Health Check') + }) + + it('creates-only plan snapshot', () => { + const changeset = { + creates: [ + {action: 'create' as const, resourceType: 'tag' as const, refKey: 'prod'}, + {action: 'create' as const, resourceType: 'monitor' as const, refKey: 'API Health'}, + ], + updates: [], deletes: [], memberships: [], + } + const result = formatPlan(changeset) + expect(result).toMatchInlineSnapshot(` + "Plan: 2 to create, 0 to update, 0 to delete, 0 memberships + + + tag "prod" + + monitor "API Health"" + `) + }) + + it('deletes-only plan snapshot', () => { + const changeset = { + creates: [], + updates: [], + deletes: [ + {action: 'delete' as const, resourceType: 'monitor' as const, refKey: 'Old', existingId: 'm-1'}, + {action: 'delete' as const, resourceType: 'tag' as const, refKey: 'Unused', existingId: 't-1'}, + ], + memberships: [], + } + const result = formatPlan(changeset) + expect(result).toMatchInlineSnapshot(` + "Plan: 0 to create, 0 to update, 2 to delete, 0 memberships + + - monitor "Old" + - tag "Unused"" + `) + }) + + it('mixed plan snapshot', () => { + const changeset = { + creates: [{action: 'create' as const, resourceType: 'tag' as const, refKey: 'new-tag'}], + updates: [{action: 'update' as const, resourceType: 'monitor' as const, refKey: 'API', existingId: 'm-1'}], + deletes: [{action: 'delete' as const, resourceType: 'secret' as const, refKey: 'old-key', existingId: 's-1'}], + memberships: [{action: 'create' as const, resourceType: 'groupMembership' as const, refKey: 'G → API'}], + } + const result = formatPlan(changeset) + expect(result).toMatchInlineSnapshot(` + "Plan: 1 to create, 1 to update, 1 to delete, 1 memberships + + + tag "new-tag" + ~ monitor "API" + - secret "old-key" + → G → API" + `) + }) + }) +}) diff --git a/test/yaml/entitlements.test.ts b/test/yaml/entitlements.test.ts new file mode 100644 index 0000000..0042e5d --- /dev/null +++ b/test/yaml/entitlements.test.ts @@ -0,0 +1,185 @@ +import {describe, it, expect, vi, beforeEach} from 'vitest' + +vi.mock('../../src/lib/typed-api.js', () => ({ + typedGet: vi.fn(), + typedPost: vi.fn(), + typedPut: vi.fn(), + typedPatch: vi.fn(), + typedDelete: vi.fn(), + fetchPaginated: vi.fn(), +})) + +import type {ApiClient} from '../../src/lib/api-client.js' +import {checkEntitlements, formatEntitlementWarnings} from '../../src/lib/yaml/entitlements.js' +import {typedGet} from '../../src/lib/typed-api.js' +import type {Changeset} from '../../src/lib/yaml/differ.js' +import type {EntitlementWarning} from '../../src/lib/yaml/entitlements.js' + +const mockTypedGet = vi.mocked(typedGet) +const fakeClient = {} as ApiClient + +function monitorCreates(n: number): Changeset { + const creates = Array.from({length: n}, (_, i) => ({ + action: 'create' as const, + resourceType: 'monitor' as const, + refKey: `m${i}`, + })) + return {creates, updates: [], deletes: [], memberships: []} +} + +describe('entitlements', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('formatEntitlementWarnings', () => { + it('formats single warning', () => { + const warnings: EntitlementWarning[] = [{ + resource: 'monitors', current: 48, creating: 5, limit: 50, + }] + const output = formatEntitlementWarnings(warnings) + expect(output).toContain('monitors') + expect(output).toContain('5 new') + expect(output).toContain('2 remaining') + }) + + it('formats multiple warnings', () => { + const warnings: EntitlementWarning[] = [ + {resource: 'monitors', current: 9, creating: 3, limit: 10}, + {resource: 'webhooks', current: 4, creating: 2, limit: 5}, + ] + const output = formatEntitlementWarnings(warnings) + expect(output).toContain('monitors') + expect(output).toContain('webhooks') + expect(output.split('\n')).toHaveLength(2) + }) + + it('returns empty string for no warnings', () => { + expect(formatEntitlementWarnings([])).toBe('') + }) + }) + + describe('checkEntitlements', () => { + it('returns null on API error', async () => { + mockTypedGet.mockRejectedValueOnce(new Error('network')) + const result = await checkEntitlements(fakeClient, monitorCreates(1)) + expect(result).toBeNull() + }) + + it('returns null when plan data is missing', async () => { + mockTypedGet.mockResolvedValueOnce({data: {plan: null}}) + const result = await checkEntitlements(fakeClient, monitorCreates(1)) + expect(result).toBeNull() + }) + + it('returns null when entitlements are missing', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: {plan: {tier: 'FREE', usage: {monitors: 5}}}, + }) + const result = await checkEntitlements(fakeClient, monitorCreates(1)) + expect(result).toBeNull() + }) + + it('detects over-limit creates', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: { + plan: { + tier: 'FREE', + entitlements: {monitors: {value: 10}}, + usage: {monitors: 8}, + }, + organization: {name: 'TestOrg'}, + }, + }) + const result = await checkEntitlements(fakeClient, monitorCreates(5)) + expect(result).not.toBeNull() + expect(result!.warnings).toHaveLength(1) + expect(result!.warnings[0]).toMatchObject({ + resource: 'monitors', + current: 8, + creating: 5, + limit: 10, + }) + }) + + it('no warnings when under limit', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: { + plan: { + tier: 'FREE', + entitlements: {monitors: {value: 10}}, + usage: {monitors: 8}, + }, + organization: {name: 'TestOrg'}, + }, + }) + const result = await checkEntitlements(fakeClient, monitorCreates(1)) + expect(result).not.toBeNull() + expect(result!.warnings).toHaveLength(0) + }) + + it('skips unlimited entitlements', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: { + plan: { + tier: 'FREE', + entitlements: {monitors: {value: Number.MAX_SAFE_INTEGER}}, + usage: {monitors: 8}, + }, + }, + }) + const result = await checkEntitlements(fakeClient, monitorCreates(100)) + expect(result).not.toBeNull() + expect(result!.warnings).toHaveLength(0) + }) + + it('builds header correctly', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: { + plan: { + tier: 'FREE', + entitlements: {monitors: {value: 10}}, + usage: {monitors: 8}, + }, + organization: {name: 'TestOrg'}, + }, + }) + const result = await checkEntitlements(fakeClient, monitorCreates(0)) + expect(result).not.toBeNull() + expect(result!.header).toContain('FREE') + expect(result!.header).toContain('TestOrg') + expect(result!.header).toMatch(/monitors:\s*8\/10/) + }) + + it('handles multiple resource types', async () => { + mockTypedGet.mockResolvedValueOnce({ + data: { + plan: { + tier: 'FREE', + entitlements: { + monitors: {value: 10}, + webhooks: {value: 5}, + }, + usage: {monitors: 9, webhooks: 4}, + }, + }, + }) + const changeset: Changeset = { + creates: [ + {action: 'create', resourceType: 'monitor', refKey: 'a'}, + {action: 'create', resourceType: 'monitor', refKey: 'b'}, + {action: 'create', resourceType: 'webhook', refKey: 'u1'}, + {action: 'create', resourceType: 'webhook', refKey: 'u2'}, + ], + updates: [], + deletes: [], + memberships: [], + } + const result = await checkEntitlements(fakeClient, changeset) + expect(result).not.toBeNull() + expect(result!.warnings).toHaveLength(2) + const resources = result!.warnings.map((w) => w.resource).sort() + expect(resources).toEqual(['monitors', 'webhooks']) + }) + }) +}) diff --git a/test/yaml/handlers.test.ts b/test/yaml/handlers.test.ts new file mode 100644 index 0000000..688c756 --- /dev/null +++ b/test/yaml/handlers.test.ts @@ -0,0 +1,131 @@ +/** + * Tests for the handler registry and handler completeness. + * Verifies that: + * 1. Every HandledResourceType has a registered handler + * 2. Handler metadata (refType, listPath, configKey) is correct + * 3. fetchAll, getRefKey, getApiRefKey, getApiId, deletePath return expected values + */ +import {describe, it, expect} from 'vitest' +import {HANDLER_MAP, getHandler, allHandlers} from '../../src/lib/yaml/handlers.js' +import type {HandledResourceType} from '../../src/lib/yaml/types.js' +import {YAML_SECTION_KEYS} from '../../src/lib/yaml/schema.js' + +const ALL_HANDLED_TYPES: HandledResourceType[] = [ + 'tag', 'environment', 'secret', 'alertChannel', + 'notificationPolicy', 'webhook', 'resourceGroup', + 'monitor', 'dependency', +] + +describe('handler registry', () => { + it('HANDLER_MAP has an entry for every HandledResourceType', () => { + for (const type of ALL_HANDLED_TYPES) { + expect(HANDLER_MAP[type], `missing handler for ${type}`).toBeDefined() + expect(HANDLER_MAP[type].resourceType).toBe(type) + } + }) + + it('getHandler returns the correct handler', () => { + for (const type of ALL_HANDLED_TYPES) { + const h = getHandler(type) + expect(h.resourceType).toBe(type) + } + }) + + it('allHandlers returns all 9 handlers', () => { + const handlers = allHandlers() + expect(handlers).toHaveLength(9) + const types = new Set(handlers.map((h) => h.resourceType)) + for (const type of ALL_HANDLED_TYPES) { + expect(types.has(type), `allHandlers() missing ${type}`).toBe(true) + } + }) + + it('every handler configKey is a valid YAML_SECTION_KEY', () => { + for (const handler of allHandlers()) { + expect( + (YAML_SECTION_KEYS as readonly string[]).includes(handler.configKey), + `${handler.resourceType}.configKey="${handler.configKey}" is not in YAML_SECTION_KEYS`, + ).toBe(true) + } + }) + + it('every handler has a fetchAll method', () => { + for (const handler of allHandlers()) { + expect(typeof handler.fetchAll, `${handler.resourceType} missing fetchAll`).toBe('function') + } + }) +}) + +describe('handler metadata', () => { + it.each([ + ['tag', 'tags', 'tags', '/api/v1/tags'], + ['environment', 'environments', 'environments', '/api/v1/environments'], + ['secret', 'secrets', 'secrets', '/api/v1/secrets'], + ['alertChannel', 'alertChannels', 'alertChannels', '/api/v1/alert-channels'], + ['notificationPolicy', 'notificationPolicies', 'notificationPolicies', '/api/v1/notification-policies'], + ['webhook', 'webhooks', 'webhooks', '/api/v1/webhooks'], + ['resourceGroup', 'resourceGroups', 'resourceGroups', '/api/v1/resource-groups'], + ['monitor', 'monitors', 'monitors', '/api/v1/monitors'], + ['dependency', 'dependencies', 'dependencies', '/api/v1/service-subscriptions'], + ] as const)('%s → refType=%s, configKey=%s, listPath=%s', (type, refType, configKey, listPath) => { + const h = getHandler(type) + expect(h.refType).toBe(refType) + expect(h.configKey).toBe(configKey) + expect(h.listPath).toBe(listPath) + }) +}) + +describe('handler getRefKey', () => { + it('tag uses name', () => expect(getHandler('tag').getRefKey({name: 'prod'})).toBe('prod')) + it('environment uses slug', () => expect(getHandler('environment').getRefKey({slug: 'staging', name: 'S'})).toBe('staging')) + it('secret uses key', () => expect(getHandler('secret').getRefKey({key: 'api-key', value: 'x'})).toBe('api-key')) + it('alertChannel uses name', () => expect(getHandler('alertChannel').getRefKey({name: 'slack'})).toBe('slack')) + it('notificationPolicy uses name', () => expect(getHandler('notificationPolicy').getRefKey({name: 'p'})).toBe('p')) + it('webhook uses url', () => expect(getHandler('webhook').getRefKey({url: 'https://x.com'})).toBe('https://x.com')) + it('resourceGroup uses name', () => expect(getHandler('resourceGroup').getRefKey({name: 'API'})).toBe('API')) + it('monitor uses name', () => expect(getHandler('monitor').getRefKey({name: 'M'})).toBe('M')) + it('dependency uses service slug', () => expect(getHandler('dependency').getRefKey({service: 'gh'})).toBe('gh')) +}) + +describe('handler getApiRefKey + getApiId', () => { + it('tag extracts name and id', () => { + const h = getHandler('tag') + expect(h.getApiRefKey({name: 'prod', id: 'tag-1'})).toBe('prod') + expect(h.getApiId({id: 'tag-1'})).toBe('tag-1') + }) + + it('environment extracts slug and id', () => { + const h = getHandler('environment') + expect(h.getApiRefKey({slug: 'staging'})).toBe('staging') + expect(h.getApiId({id: 'env-1'})).toBe('env-1') + }) + + it('monitor extracts name, id, and managedBy', () => { + const h = getHandler('monitor') + expect(h.getApiRefKey({name: 'M'})).toBe('M') + expect(h.getApiId({id: 'mon-1'})).toBe('mon-1') + expect(h.getManagedBy!({managedBy: 'CLI'})).toBe('CLI') + }) + + it('dependency extracts slug and subscriptionId', () => { + const h = getHandler('dependency') + expect(h.getApiRefKey({slug: 'gh'})).toBe('gh') + expect(h.getApiId({subscriptionId: 'sub-1'})).toBe('sub-1') + }) +}) + +describe('handler deletePath', () => { + it.each([ + ['tag', '/api/v1/tags/id-1'], + ['environment', '/api/v1/environments/id-1'], + ['secret', '/api/v1/secrets/id-1'], + ['alertChannel', '/api/v1/alert-channels/id-1'], + ['notificationPolicy', '/api/v1/notification-policies/id-1'], + ['webhook', '/api/v1/webhooks/id-1'], + ['resourceGroup', '/api/v1/resource-groups/id-1'], + ['monitor', '/api/v1/monitors/id-1'], + ['dependency', '/api/v1/service-subscriptions/id-1'], + ] as const)('%s → %s', (type, expectedPath) => { + expect(getHandler(type).deletePath('id-1')).toBe(expectedPath) + }) +}) diff --git a/test/yaml/interpolation.test.ts b/test/yaml/interpolation.test.ts new file mode 100644 index 0000000..37d462e --- /dev/null +++ b/test/yaml/interpolation.test.ts @@ -0,0 +1,135 @@ +import {describe, it, expect} from 'vitest' +import {interpolate, findVariables, findMissingVariables, InterpolationError} from '../../src/lib/yaml/interpolation.js' + +describe('interpolation', () => { + describe('interpolate', () => { + it('replaces ${VAR} with env value', () => { + const result = interpolate('url: ${API_URL}', {API_URL: 'https://api.com'}) + expect(result).toBe('url: https://api.com') + }) + + it('replaces multiple vars in one string', () => { + const result = interpolate('${HOST}:${PORT}', {HOST: 'localhost', PORT: '3000'}) + expect(result).toBe('localhost:3000') + }) + + it('uses fallback from ${VAR:-default}', () => { + const result = interpolate('url: ${API_URL:-https://default.com}', {}) + expect(result).toBe('url: https://default.com') + }) + + it('prefers env value over fallback', () => { + const result = interpolate('${VAR:-fallback}', {VAR: 'actual'}) + expect(result).toBe('actual') + }) + + it('uses fallback for empty string value', () => { + const result = interpolate('${VAR:-fallback}', {VAR: ''}) + expect(result).toBe('fallback') + }) + + it('throws InterpolationError for missing required var', () => { + expect(() => interpolate('${MISSING}', {})).toThrow(InterpolationError) + }) + + it('throws with helpful message', () => { + try { + interpolate('${SECRET_KEY}', {}) + expect.fail('should have thrown') + } catch (err) { + expect(err).toBeInstanceOf(InterpolationError) + expect((err as InterpolationError).variable).toBe('SECRET_KEY') + expect((err as InterpolationError).message).toContain('SECRET_KEY') + } + }) + + it('leaves strings without ${} unchanged', () => { + const result = interpolate('plain text here', {}) + expect(result).toBe('plain text here') + }) + + it('handles empty fallback', () => { + const result = interpolate('pre-${VAR:-}-post', {}) + expect(result).toBe('pre--post') + }) + + it('handles fallback with special characters', () => { + const result = interpolate('${VAR:-https://hooks.slack.com/T00/B00/x}', {}) + expect(result).toBe('https://hooks.slack.com/T00/B00/x') + }) + + it('handles nested-like braces (outer only)', () => { + const result = interpolate('${OUTER}', {OUTER: 'value'}) + expect(result).toBe('value') + }) + + it('handles unicode variable names', () => { + const result = interpolate('${MY_VAR_123}', {MY_VAR_123: 'ok'}) + expect(result).toBe('ok') + }) + + it('handles multiple same var occurrences', () => { + const result = interpolate('${A}-${A}', {A: 'x'}) + expect(result).toBe('x-x') + }) + + it('handles whitespace in var name (trimmed)', () => { + const result = interpolate('${ MY_VAR }', {MY_VAR: 'trimmed'}) + expect(result).toBe('trimmed') + }) + + it('handles multiline input', () => { + const input = 'line1: ${A}\nline2: ${B:-default}' + const result = interpolate(input, {A: 'first'}) + expect(result).toBe('line1: first\nline2: default') + }) + }) + + describe('findVariables', () => { + it('finds all variable references', () => { + const vars = findVariables('${A} and ${B:-default} and ${C}') + expect(vars).toEqual(['A', 'B', 'C']) + }) + + it('returns empty for no vars', () => { + expect(findVariables('no vars here')).toEqual([]) + }) + + it('finds duplicates', () => { + const vars = findVariables('${A} ${A}') + expect(vars).toEqual(['A', 'A']) + }) + }) + + describe('findMissingVariables', () => { + it('returns only missing required vars', () => { + const missing = findMissingVariables('${SET} ${MISSING} ${DEFAULT:-ok}', {SET: 'yes'}) + expect(missing).toEqual(['MISSING']) + }) + + it('returns empty when all vars are set', () => { + const missing = findMissingVariables('${A} ${B:-x}', {A: 'val'}) + expect(missing).toEqual([]) + }) + + it('flags empty string as missing', () => { + const missing = findMissingVariables('${EMPTY}', {EMPTY: ''}) + expect(missing).toEqual(['EMPTY']) + }) + + it('returns duplicate occurrences of the same missing var', () => { + const missing = findMissingVariables('${X} ${X}', {}) + expect(missing).toEqual(['X', 'X']) + }) + + it('ignores vars with defaults', () => { + const missing = findMissingVariables('${A:-x} ${B:-y}', {}) + expect(missing).toEqual([]) + }) + + it('returns multiple distinct missing vars', () => { + const missing = findMissingVariables('${A} ${B} ${C}', {}) + expect(missing).toEqual(['A', 'B', 'C']) + }) + }) +}) diff --git a/test/yaml/parity.test.ts b/test/yaml/parity.test.ts new file mode 100644 index 0000000..1bb8191 --- /dev/null +++ b/test/yaml/parity.test.ts @@ -0,0 +1,51 @@ +/** + * Contract test: every CLI resource type must have YAML schema coverage. + * This test ensures that adding a new resource to the CLI without + * updating the YAML schema causes a test failure. + */ +import {describe, it, expect} from 'vitest' +import {YAML_SECTION_KEYS, type YamlSectionKey} from '../../src/lib/yaml/schema.js' +import * as resources from '../../src/lib/resources.js' + +const CLI_RESOURCE_CONFIGS = [ + {config: resources.MONITORS, yamlKey: 'monitors'}, + {config: resources.INCIDENTS, yamlKey: null}, + {config: resources.ALERT_CHANNELS, yamlKey: 'alertChannels'}, + {config: resources.NOTIFICATION_POLICIES, yamlKey: 'notificationPolicies'}, + {config: resources.ENVIRONMENTS, yamlKey: 'environments'}, + {config: resources.SECRETS, yamlKey: 'secrets'}, + {config: resources.TAGS, yamlKey: 'tags'}, + {config: resources.RESOURCE_GROUPS, yamlKey: 'resourceGroups'}, + {config: resources.WEBHOOKS, yamlKey: 'webhooks'}, + {config: resources.API_KEYS, yamlKey: null}, + {config: resources.DEPENDENCIES, yamlKey: 'dependencies'}, +] as const + +describe('CLI ↔ YAML parity', () => { + it('all deployable CLI resources have a YAML section key', () => { + const deployable = CLI_RESOURCE_CONFIGS.filter((r) => r.yamlKey !== null) + for (const {config, yamlKey} of deployable) { + expect( + (YAML_SECTION_KEYS as readonly string[]).includes(yamlKey), + `CLI resource "${config.name}" maps to YAML key "${yamlKey}" which is not in YAML_SECTION_KEYS`, + ).toBe(true) + } + }) + + it('YAML section keys all have a CLI resource', () => { + const coveredKeys = new Set(CLI_RESOURCE_CONFIGS.map((r) => r.yamlKey).filter(Boolean)) + for (const key of YAML_SECTION_KEYS) { + expect( + coveredKeys.has(key), + `YAML section "${key}" has no corresponding CLI resource mapping`, + ).toBe(true) + } + }) + + it('non-deployable resources (incidents, API keys) are excluded from YAML', () => { + const excluded = CLI_RESOURCE_CONFIGS.filter((r) => r.yamlKey === null) + expect(excluded.length).toBe(2) + expect(excluded.map((r) => r.config.name)).toContain('incident') + expect(excluded.map((r) => r.config.name)).toContain('API key') + }) +}) diff --git a/test/yaml/parser.test.ts b/test/yaml/parser.test.ts new file mode 100644 index 0000000..4d9fa87 --- /dev/null +++ b/test/yaml/parser.test.ts @@ -0,0 +1,162 @@ +import {describe, it, expect, afterEach} from 'vitest' +import {join, dirname} from 'node:path' +import {fileURLToPath} from 'node:url' +import {mkdirSync, rmSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {parseConfigFile, loadConfig, ParseError} from '../../src/lib/yaml/parser.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const fixtures = join(__dirname, '..', 'fixtures', 'yaml') + +const tmpDirs: string[] = [] +function makeTmpDir(): string { + const dir = join(tmpdir(), `devhelm-parser-test-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(dir, {recursive: true}) + tmpDirs.push(dir) + return dir +} + +afterEach(() => { + for (const d of tmpDirs) rmSync(d, {recursive: true, force: true}) + tmpDirs.length = 0 +}) + +describe('parser', () => { + describe('parseConfigFile', () => { + it('parses minimal valid config', () => { + const config = parseConfigFile(join(fixtures, 'valid', 'minimal.yml')) + expect(config.monitors).toHaveLength(1) + expect(config.monitors![0].name).toBe('Simple Health Check') + expect(config.monitors![0].type).toBe('HTTP') + }) + + it('parses full-stack config with all sections', () => { + const config = parseConfigFile(join(fixtures, 'valid', 'full-stack.yml')) + expect(config.tags).toHaveLength(2) + expect(config.environments).toHaveLength(2) + expect(config.secrets).toHaveLength(1) + expect(config.alertChannels).toHaveLength(3) + expect(config.notificationPolicies).toHaveLength(1) + expect(config.webhooks).toHaveLength(1) + expect(config.resourceGroups).toHaveLength(1) + expect(config.monitors).toHaveLength(7) + expect(config.dependencies).toHaveLength(2) + }) + + it('resolves env vars with fallbacks', () => { + const config = parseConfigFile(join(fixtures, 'valid', 'env-vars.yml')) + expect(config.monitors![0].config).toHaveProperty('url', 'https://default.example.com') + }) + + it('resolves env vars from environment', () => { + process.env.APP_URL = 'https://custom.example.com' + try { + const config = parseConfigFile(join(fixtures, 'valid', 'env-vars.yml')) + expect(config.monitors![0].config).toHaveProperty('url', 'https://custom.example.com') + } finally { + delete process.env.APP_URL + } + }) + + it('throws on missing file', () => { + expect(() => parseConfigFile('nonexistent.yml')).toThrow(ParseError) + }) + + it('throws on missing required env var', () => { + delete process.env.MISSING_SECRET_VALUE + expect(() => parseConfigFile(join(fixtures, 'invalid', 'missing-env-var.yml'))).toThrow(ParseError) + }) + + it('skips env var resolution when resolveEnv is false', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'missing-env-var.yml'), false) + expect(config.secrets![0].value).toContain('${MISSING_SECRET_VALUE}') + }) + + it('throws on invalid YAML syntax', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'bad.yml'), ':\n foo: [unclosed') + expect(() => parseConfigFile(join(dir, 'bad.yml'))).toThrow(ParseError) + }) + + it('throws on empty file (null parsed)', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'empty.yml'), '') + expect(() => parseConfigFile(join(dir, 'empty.yml'))).toThrow(ParseError) + }) + + it('throws on scalar YAML (not an object)', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'scalar.yml'), 'just a string') + expect(() => parseConfigFile(join(dir, 'scalar.yml'))).toThrow(ParseError) + }) + }) + + describe('loadConfig', () => { + it('loads single file', () => { + const config = loadConfig([join(fixtures, 'valid', 'minimal.yml')]) + expect(config.monitors).toHaveLength(1) + }) + + it('merges multiple files', () => { + const config = loadConfig([ + join(fixtures, 'valid', 'multi-a.yml'), + join(fixtures, 'valid', 'multi-b.yml'), + ]) + expect(config.tags).toHaveLength(2) + expect(config.monitors).toHaveLength(2) + expect(config.tags![0].name).toBe('backend') + expect(config.tags![1].name).toBe('frontend') + }) + + it('applies defaults to monitors', () => { + const config = loadConfig([join(fixtures, 'valid', 'defaults.yml')]) + expect(config.monitors![0].frequency).toBe(120) + expect(config.monitors![0].regions).toEqual(['us-east', 'eu-west']) + expect(config.monitors![1].frequency).toBe(30) + expect(config.monitors![1].regions).toEqual(['us-west']) + }) + + it('throws on empty paths', () => { + expect(() => loadConfig([])).toThrow() + }) + + it('loads directory of YAML files', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'a.yml'), 'tags:\n - name: from-a') + writeFileSync(join(dir, 'b.yaml'), 'tags:\n - name: from-b') + writeFileSync(join(dir, 'ignored.txt'), 'not yaml') + const config = loadConfig([dir]) + expect(config.tags).toHaveLength(2) + expect(config.tags!.map((t) => t.name).sort()).toEqual(['from-a', 'from-b']) + }) + + it('loads directory files in sorted order', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'z.yml'), 'tags:\n - name: z') + writeFileSync(join(dir, 'a.yml'), 'tags:\n - name: a') + const config = loadConfig([dir]) + expect(config.tags![0].name).toBe('a') + expect(config.tags![1].name).toBe('z') + }) + + it('throws on empty directory', () => { + const dir = makeTmpDir() + expect(() => loadConfig([dir])).toThrow(ParseError) + }) + + it('ignores nested directories (non-recursive)', () => { + const dir = makeTmpDir() + writeFileSync(join(dir, 'root.yml'), 'tags:\n - name: root') + const nested = join(dir, 'nested') + mkdirSync(nested) + writeFileSync(join(nested, 'child.yml'), 'tags:\n - name: child') + const config = loadConfig([dir]) + expect(config.tags).toHaveLength(1) + expect(config.tags![0].name).toBe('root') + }) + + it('throws on nonexistent path', () => { + expect(() => loadConfig(['/nonexistent/path'])).toThrow(ParseError) + }) + }) +}) diff --git a/test/yaml/resolver.test.ts b/test/yaml/resolver.test.ts new file mode 100644 index 0000000..8e7d274 --- /dev/null +++ b/test/yaml/resolver.test.ts @@ -0,0 +1,94 @@ +import {describe, it, expect} from 'vitest' +import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' + +describe('ResolvedRefs', () => { + it('get returns undefined for unset ref', () => { + const refs = new ResolvedRefs() + expect(refs.get('tags', 'foo')).toBeUndefined() + }) + + it('get returns entry after set', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'prod', {id: 'tag-1', refKey: 'prod', raw: {name: 'prod'}}) + const entry = refs.get('tags', 'prod') + expect(entry).toBeDefined() + expect(entry!.id).toBe('tag-1') + expect(entry!.refKey).toBe('prod') + expect(entry!.raw).toEqual({name: 'prod'}) + }) + + it('resolve returns id for existing ref', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'api', {id: 'mon-1', refKey: 'api', raw: {}}) + expect(refs.resolve('monitors', 'api')).toBe('mon-1') + }) + + it('resolve returns undefined for missing ref', () => { + const refs = new ResolvedRefs() + expect(refs.resolve('monitors', 'missing')).toBeUndefined() + }) + + it('require returns id for existing ref', () => { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'slack', {id: 'ch-1', refKey: 'slack', raw: {}}) + expect(refs.require('alertChannels', 'slack')).toBe('ch-1') + }) + + it('require throws for missing ref', () => { + const refs = new ResolvedRefs() + expect(() => refs.require('alertChannels', 'missing')) + .toThrow('Cannot resolve alertChannels reference "missing"') + }) + + it('all returns empty map for unset type', () => { + const refs = new ResolvedRefs() + const map = refs.all('tags') + expect(map.size).toBe(0) + }) + + it('all returns map with all entries for type', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'a', {id: '1', refKey: 'a', raw: {}}) + refs.set('tags', 'b', {id: '2', refKey: 'b', raw: {}}) + const map = refs.all('tags') + expect(map.size).toBe(2) + expect(map.get('a')!.id).toBe('1') + expect(map.get('b')!.id).toBe('2') + }) + + it('allEntries returns array of entries', () => { + const refs = new ResolvedRefs() + refs.set('environments', 'prod', {id: 'e-1', refKey: 'prod', raw: {}}) + refs.set('environments', 'staging', {id: 'e-2', refKey: 'staging', raw: {}}) + const entries = refs.allEntries('environments') + expect(entries).toHaveLength(2) + expect(entries.map((e) => e.refKey).sort()).toEqual(['prod', 'staging']) + }) + + it('allEntries returns empty for unset type', () => { + const refs = new ResolvedRefs() + expect(refs.allEntries('secrets')).toHaveLength(0) + }) + + it('set overwrites existing entry', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'prod', {id: '1', refKey: 'prod', raw: {}}) + refs.set('tags', 'prod', {id: '2', refKey: 'prod', raw: {updated: true}}) + expect(refs.resolve('tags', 'prod')).toBe('2') + expect(refs.get('tags', 'prod')!.raw).toEqual({updated: true}) + }) + + it('types are isolated', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'same-key', {id: 'tag-1', refKey: 'same-key', raw: {}}) + refs.set('monitors', 'same-key', {id: 'mon-1', refKey: 'same-key', raw: {}}) + expect(refs.resolve('tags', 'same-key')).toBe('tag-1') + expect(refs.resolve('monitors', 'same-key')).toBe('mon-1') + }) + + it('stores managedBy metadata', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'api', {id: 'mon-1', refKey: 'api', managedBy: 'CLI', raw: {}}) + expect(refs.get('monitors', 'api')!.managedBy).toBe('CLI') + }) +}) diff --git a/test/yaml/state.test.ts b/test/yaml/state.test.ts new file mode 100644 index 0000000..e0dd30e --- /dev/null +++ b/test/yaml/state.test.ts @@ -0,0 +1,99 @@ +import {describe, it, expect, beforeEach, afterEach} from 'vitest' +import {mkdirSync, rmSync, existsSync, writeFileSync} from 'node:fs' +import {join} from 'node:path' +import {tmpdir} from 'node:os' +import {readState, writeState, buildState} from '../../src/lib/yaml/state.js' + +describe('state', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = join(tmpdir(), `devhelm-test-${Date.now()}`) + mkdirSync(tmpDir, {recursive: true}) + }) + + afterEach(() => { + rmSync(tmpDir, {recursive: true, force: true}) + }) + + it('returns undefined when state file does not exist', () => { + expect(readState(tmpDir)).toBeUndefined() + }) + + it('writes and reads state', () => { + const state = buildState([ + {resourceType: 'monitor', refKey: 'test', id: 'mon-1', createdAt: '2025-01-01'}, + ]) + writeState(state, tmpDir) + expect(existsSync(join(tmpDir, '.devhelm', 'state.json'))).toBe(true) + + const loaded = readState(tmpDir) + expect(loaded).toBeDefined() + expect(loaded!.version).toBe('1') + expect(loaded!.resources).toHaveLength(1) + expect(loaded!.resources[0].refKey).toBe('test') + }) + + it('creates .devhelm directory if missing', () => { + const state = buildState([]) + writeState(state, tmpDir) + expect(existsSync(join(tmpDir, '.devhelm'))).toBe(true) + }) + + it('buildState sets lastDeployedAt', () => { + const before = Date.now() + const state = buildState([]) + const after = Date.now() + const ts = new Date(state.lastDeployedAt).getTime() + expect(ts).toBeGreaterThanOrEqual(before) + expect(ts).toBeLessThanOrEqual(after) + }) + + it('returns undefined on corrupt JSON', () => { + const dir = join(tmpDir, '.devhelm') + mkdirSync(dir, {recursive: true}) + writeFileSync(join(dir, 'state.json'), 'not valid json {{{') + expect(readState(tmpDir)).toBeUndefined() + }) + + it('overwrites previous state on write', () => { + const state1 = buildState([ + {resourceType: 'tag', refKey: 'A', id: 't-1', createdAt: '2025-01-01'}, + ]) + writeState(state1, tmpDir) + expect(readState(tmpDir)!.resources).toHaveLength(1) + + const state2 = buildState([ + {resourceType: 'tag', refKey: 'A', id: 't-1', createdAt: '2025-01-01'}, + {resourceType: 'monitor', refKey: 'B', id: 'm-1', createdAt: '2025-01-02'}, + ]) + writeState(state2, tmpDir) + const loaded = readState(tmpDir)! + expect(loaded.resources).toHaveLength(2) + }) + + it('accumulates state across multiple deploys', () => { + const deploy1 = buildState([ + {resourceType: 'tag', refKey: 'A', id: 't-1', createdAt: '2025-01-01'}, + ]) + writeState(deploy1, tmpDir) + + const existing = readState(tmpDir)! + const combined = buildState([ + ...existing.resources, + {resourceType: 'monitor', refKey: 'M', id: 'm-1', createdAt: '2025-01-02'}, + ]) + writeState(combined, tmpDir) + + const final = readState(tmpDir)! + expect(final.resources).toHaveLength(2) + expect(final.resources.map((r) => r.refKey).sort()).toEqual(['A', 'M']) + }) + + it('buildState with empty entries creates valid state', () => { + const state = buildState([]) + expect(state.version).toBe('1') + expect(state.resources).toEqual([]) + expect(state.lastDeployedAt).toBeTruthy() + }) +}) diff --git a/test/yaml/transform.test.ts b/test/yaml/transform.test.ts new file mode 100644 index 0000000..047b59e --- /dev/null +++ b/test/yaml/transform.test.ts @@ -0,0 +1,433 @@ +import {describe, it, expect} from 'vitest' +import { + toCreateTagRequest, toCreateEnvironmentRequest, toCreateSecretRequest, + toCreateAlertChannelRequest, toCreateNotificationPolicyRequest, + toCreateWebhookRequest, toCreateResourceGroupRequest, + toCreateMonitorRequest, toUpdateMonitorRequest, +} from '../../src/lib/yaml/transform.js' +import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' +import type { + YamlTag, YamlEnvironment, YamlSecret, YamlAlertChannel, + YamlNotificationPolicy, YamlWebhook, YamlResourceGroup, YamlMonitor, +} from '../../src/lib/yaml/schema.js' + +function emptyRefs(): ResolvedRefs { + return new ResolvedRefs() +} + +function refsWithChannels(): ResolvedRefs { + const refs = new ResolvedRefs() + refs.set('alertChannels', 'ops-slack', {id: 'ch-123', refKey: 'ops-slack', raw: {}}) + refs.set('alertChannels', 'pagerduty', {id: 'ch-456', refKey: 'pagerduty', raw: {}}) + refs.set('tags', 'production', {id: 'tag-1', refKey: 'production', raw: {}}) + refs.set('environments', 'prod', {id: 'env-1', refKey: 'prod', raw: {}}) + refs.set('secrets', 'api-key', {id: 'sec-1', refKey: 'api-key', raw: {}}) + return refs +} + +describe('transforms', () => { + describe('toCreateTagRequest', () => { + it('transforms basic tag', () => { + const tag: YamlTag = {name: 'production', color: '#EF4444'} + const req = toCreateTagRequest(tag) + expect(req.name).toBe('production') + expect(req.color).toBe('#EF4444') + }) + + it('defaults color to null', () => { + const req = toCreateTagRequest({name: 'test'}) + expect(req.color).toBeNull() + }) + }) + + describe('toCreateEnvironmentRequest', () => { + it('transforms environment', () => { + const env: YamlEnvironment = {name: 'Production', slug: 'production', isDefault: true} + const req = toCreateEnvironmentRequest(env) + expect(req.name).toBe('Production') + expect(req.slug).toBe('production') + expect(req.isDefault).toBe(true) + }) + + it('handles variables', () => { + const env: YamlEnvironment = {name: 'Dev', slug: 'dev', variables: {API_URL: 'http://localhost'}} + const req = toCreateEnvironmentRequest(env) + expect(req.variables).toEqual({API_URL: 'http://localhost'}) + }) + + it('defaults variables to null', () => { + const env: YamlEnvironment = {name: 'Staging', slug: 'staging'} + const req = toCreateEnvironmentRequest(env) + expect(req.variables).toBeNull() + }) + + it('defaults isDefault to undefined', () => { + const env: YamlEnvironment = {name: 'CI', slug: 'ci'} + const req = toCreateEnvironmentRequest(env) + expect(req.isDefault).toBeUndefined() + }) + }) + + describe('toCreateSecretRequest', () => { + it('transforms secret', () => { + const req = toCreateSecretRequest({key: 'api-key', value: 'secret-123'}) + expect(req.key).toBe('api-key') + expect(req.value).toBe('secret-123') + }) + }) + + describe('toCreateAlertChannelRequest', () => { + it('transforms slack channel', () => { + const channel: YamlAlertChannel = { + name: 'ops', type: 'slack', + config: {webhookUrl: 'https://hooks.slack.com/test'}, + } + const req = toCreateAlertChannelRequest(channel) + expect(req.name).toBe('ops') + expect(req.config).toHaveProperty('channelType', 'SlackChannelConfig') + expect(req.config).toHaveProperty('webhookUrl', 'https://hooks.slack.com/test') + }) + + it('transforms email channel', () => { + const channel: YamlAlertChannel = { + name: 'eng', type: 'email', + config: {recipients: ['a@test.com']}, + } + const req = toCreateAlertChannelRequest(channel) + expect(req.config).toHaveProperty('channelType', 'EmailChannelConfig') + expect(req.config).toHaveProperty('recipients', ['a@test.com']) + }) + + it('transforms all 7 channel types', () => { + const types = [ + {type: 'slack' as const, config: {webhookUrl: 'url'}, expected: 'SlackChannelConfig'}, + {type: 'discord' as const, config: {webhookUrl: 'url'}, expected: 'DiscordChannelConfig'}, + {type: 'email' as const, config: {recipients: ['a@b.com']}, expected: 'EmailChannelConfig'}, + {type: 'pagerduty' as const, config: {routingKey: 'key'}, expected: 'PagerDutyChannelConfig'}, + {type: 'opsgenie' as const, config: {apiKey: 'key'}, expected: 'OpsGenieChannelConfig'}, + {type: 'teams' as const, config: {webhookUrl: 'url'}, expected: 'TeamsChannelConfig'}, + {type: 'webhook' as const, config: {url: 'url'}, expected: 'WebhookChannelConfig'}, + ] + for (const {type, config, expected} of types) { + const req = toCreateAlertChannelRequest({name: 'test', type, config}) + expect(req.config).toHaveProperty('channelType', expected) + } + }) + + it('preserves extra config fields', () => { + const channel: YamlAlertChannel = { + name: 'pg', type: 'pagerduty', + config: {routingKey: 'r-key', severity: 'critical'}, + } + const req = toCreateAlertChannelRequest(channel) + expect(req.config).toHaveProperty('routingKey', 'r-key') + expect(req.config).toHaveProperty('severity', 'critical') + }) + }) + + describe('toCreateNotificationPolicyRequest', () => { + it('transforms with channel references', () => { + const refs = refsWithChannels() + const policy: YamlNotificationPolicy = { + name: 'test', enabled: true, priority: 1, + escalation: { + steps: [{channels: ['ops-slack'], delayMinutes: 0}], + }, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.name).toBe('test') + expect(req.enabled).toBe(true) + expect(req.priority).toBe(1) + expect(req.escalation.steps[0].channelIds).toEqual(['ch-123']) + }) + + it('throws on unresolved channel', () => { + const refs = emptyRefs() + const policy: YamlNotificationPolicy = { + name: 'test', + escalation: {steps: [{channels: ['nonexistent']}]}, + } + expect(() => toCreateNotificationPolicyRequest(policy, refs)).toThrow('Cannot resolve') + }) + + it('transforms match rules with monitor names', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'api', {id: 'mon-1', refKey: 'api', raw: {}}) + refs.set('alertChannels', 'ch', {id: 'ch-1', refKey: 'ch', raw: {}}) + const policy: YamlNotificationPolicy = { + name: 'test', + matchRules: [{type: 'monitor_id_in', monitorNames: ['api']}], + escalation: {steps: [{channels: ['ch']}]}, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.matchRules![0].monitorIds).toEqual(['mon-1']) + }) + + it('defaults enabled to true', () => { + const refs = refsWithChannels() + const policy: YamlNotificationPolicy = { + name: 'p', + escalation: {steps: [{channels: ['ops-slack']}]}, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.enabled).toBe(true) + }) + + it('defaults priority to 0', () => { + const refs = refsWithChannels() + const policy: YamlNotificationPolicy = { + name: 'p', + escalation: {steps: [{channels: ['ops-slack']}]}, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.priority).toBe(0) + }) + + it('transforms multiple escalation steps with delays', () => { + const refs = refsWithChannels() + const policy: YamlNotificationPolicy = { + name: 'esc', + escalation: { + steps: [ + {channels: ['ops-slack'], delayMinutes: 0}, + {channels: ['pagerduty'], delayMinutes: 15, requireAck: true}, + ], + }, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.escalation.steps).toHaveLength(2) + expect(req.escalation.steps[0].delayMinutes).toBe(0) + expect(req.escalation.steps[1].delayMinutes).toBe(15) + expect(req.escalation.steps[1].requireAck).toBe(true) + expect(req.escalation.steps[1].channelIds).toEqual(['ch-456']) + }) + + it('transforms escalation onResolve/onReopen', () => { + const refs = refsWithChannels() + const policy: YamlNotificationPolicy = { + name: 'p', + escalation: { + steps: [{channels: ['ops-slack']}], + onResolve: 'notify_all', + onReopen: 'notify_first', + }, + } + const req = toCreateNotificationPolicyRequest(policy, refs) + expect(req.escalation.onResolve).toBe('notify_all') + expect(req.escalation.onReopen).toBe('notify_first') + }) + }) + + describe('toCreateWebhookRequest', () => { + it('transforms webhook', () => { + const webhook: YamlWebhook = { + url: 'https://hooks.example.com', events: ['monitor.down'], description: 'test', + } + const req = toCreateWebhookRequest(webhook) + expect(req.url).toBe('https://hooks.example.com') + expect(req.subscribedEvents).toEqual(['monitor.down']) + expect(req.description).toBe('test') + }) + + it('transforms webhook without description', () => { + const webhook: YamlWebhook = {url: 'https://x.com', events: ['a', 'b']} + const req = toCreateWebhookRequest(webhook) + expect(req.description).toBeUndefined() + expect(req.subscribedEvents).toEqual(['a', 'b']) + }) + }) + + describe('toCreateResourceGroupRequest', () => { + it('transforms with defaults', () => { + const refs = refsWithChannels() + refs.set('notificationPolicies', 'critical', {id: 'pol-1', refKey: 'critical', raw: {}}) + const group: YamlResourceGroup = { + name: 'API', + description: 'API services', + defaultFrequency: 30, + defaultRegions: ['us-east'], + defaultAlertChannels: ['ops-slack'], + defaultEnvironment: 'prod', + alertPolicy: 'critical', + healthThresholdType: 'PERCENTAGE', + healthThresholdValue: 80, + } + const req = toCreateResourceGroupRequest(group, refs) + expect(req.name).toBe('API') + expect(req.description).toBe('API services') + expect(req.defaultFrequency).toBe(30) + expect(req.defaultRegions).toEqual(['us-east']) + expect(req.alertPolicyId).toBe('pol-1') + expect(req.defaultEnvironmentId).toBe('env-1') + expect(req.healthThresholdType).toBe('PERCENTAGE') + expect(req.healthThresholdValue).toBe(80) + }) + + it('transforms minimal group with null defaults', () => { + const refs = emptyRefs() + const group: YamlResourceGroup = {name: 'Minimal'} + const req = toCreateResourceGroupRequest(group, refs) + expect(req.name).toBe('Minimal') + expect(req.description).toBeNull() + expect(req.alertPolicyId).toBeNull() + expect(req.defaultFrequency).toBeNull() + expect(req.defaultRegions).toBeNull() + }) + + it('transforms retry strategy', () => { + const refs = emptyRefs() + const group: YamlResourceGroup = { + name: 'G', + defaultRetryStrategy: {type: 'LINEAR', maxRetries: 3, interval: 10}, + } + const req = toCreateResourceGroupRequest(group, refs) + expect(req.defaultRetryStrategy).toEqual({type: 'LINEAR', maxRetries: 3, interval: 10}) + }) + }) + + describe('toCreateMonitorRequest', () => { + it('transforms HTTP monitor', () => { + const refs = refsWithChannels() + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + frequency: 60, + regions: ['us-east'], + tags: ['production'], + alertChannels: ['ops-slack'], + environment: 'prod', + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.name).toBe('Test') + expect(req.type).toBe('HTTP') + expect(req.managedBy).toBe('CLI') + expect(req.frequencySeconds).toBe(60) + expect(req.regions).toEqual(['us-east']) + expect(req.alertChannelIds).toEqual(['ch-123']) + expect(req.environmentId).toBe('env-1') + expect(req.tags!.tagIds).toEqual(['tag-1']) + }) + + it('creates new tags when not resolved', () => { + const refs = emptyRefs() + refs.set('alertChannels', 'ch', {id: 'ch-1', refKey: 'ch', raw: {}}) + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + tags: ['new-tag'], + alertChannels: ['ch'], + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.tags!.newTags).toEqual([{name: 'new-tag'}]) + }) + + it('mixes existing and new tags', () => { + const refs = new ResolvedRefs() + refs.set('tags', 'existing', {id: 'tag-1', refKey: 'existing', raw: {}}) + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + tags: ['existing', 'brand-new'], + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.tags!.tagIds).toEqual(['tag-1']) + expect(req.tags!.newTags).toEqual([{name: 'brand-new'}]) + }) + + it('transforms all 4 auth types', () => { + const refs = refsWithChannels() + const authTypes = [ + {type: 'BearerAuthConfig' as const, expectedType: 'BearerAuthConfig'}, + {type: 'BasicAuthConfig' as const, expectedType: 'BasicAuthConfig'}, + {type: 'ApiKeyAuthConfig' as const, expectedType: 'ApiKeyAuthConfig', headerName: 'X-Key'}, + {type: 'HeaderAuthConfig' as const, expectedType: 'HeaderAuthConfig', headerName: 'Authorization'}, + ] + for (const at of authTypes) { + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: at.type, secret: 'api-key', headerName: at.headerName}, + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.auth).toHaveProperty('type', at.expectedType) + expect(req.auth).toHaveProperty('vaultSecretId', 'sec-1') + } + }) + + it('transforms incident policy', () => { + const refs = emptyRefs() + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', count: 3, scope: 'per_region', severity: 'down'}], + confirmation: {type: 'multi_region', minRegionsFailing: 2}, + recovery: {consecutiveSuccesses: 2}, + }, + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.incidentPolicy!.triggerRules[0].type).toBe('consecutive_failures') + expect(req.incidentPolicy!.triggerRules[0].count).toBe(3) + expect(req.incidentPolicy!.confirmation.minRegionsFailing).toBe(2) + expect(req.incidentPolicy!.recovery.consecutiveSuccesses).toBe(2) + }) + + it('transforms monitor without frequency (undefined)', () => { + const refs = emptyRefs() + const monitor: YamlMonitor = { + name: 'NoFreq', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.frequencySeconds).toBeUndefined() + }) + + it('transforms monitor without tags (undefined)', () => { + const refs = emptyRefs() + const monitor: YamlMonitor = { + name: 'NoTags', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.tags).toBeUndefined() + }) + + it('transforms monitor without alertChannels', () => { + const refs = emptyRefs() + const monitor: YamlMonitor = { + name: 'Bare', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + } + const req = toCreateMonitorRequest(monitor, refs) + expect(req.alertChannelIds).toBeNull() + }) + }) + + describe('toUpdateMonitorRequest', () => { + it('includes managedBy CLI', () => { + const refs = emptyRefs() + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + } + const req = toUpdateMonitorRequest(monitor, refs) + expect(req.managedBy).toBe('CLI') + }) + + it('preserves all fields same as create', () => { + const refs = refsWithChannels() + const monitor: YamlMonitor = { + name: 'Test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + frequency: 30, regions: ['eu-west'], + tags: ['production'], alertChannels: ['ops-slack'], + } + const req = toUpdateMonitorRequest(monitor, refs) + expect(req.name).toBe('Test') + expect(req.frequencySeconds).toBe(30) + expect(req.regions).toEqual(['eu-west']) + expect(req.tags!.tagIds).toEqual(['tag-1']) + expect(req.alertChannelIds).toEqual(['ch-123']) + }) + }) +}) diff --git a/test/yaml/validator.test.ts b/test/yaml/validator.test.ts new file mode 100644 index 0000000..c1746dc --- /dev/null +++ b/test/yaml/validator.test.ts @@ -0,0 +1,784 @@ +import {describe, it, expect} from 'vitest' +import {join, dirname} from 'node:path' +import {fileURLToPath} from 'node:url' +import {parseConfigFile} from '../../src/lib/yaml/parser.js' +import {validate} from '../../src/lib/yaml/validator.js' +import type {DevhelmConfig} from '../../src/lib/yaml/schema.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const fixtures = join(__dirname, '..', 'fixtures', 'yaml') + +describe('validator', () => { + describe('valid configs', () => { + it('passes minimal config', () => { + const config = parseConfigFile(join(fixtures, 'valid', 'minimal.yml')) + const result = validate(config) + expect(result.errors).toHaveLength(0) + }) + + it('passes full-stack config', () => { + const config = parseConfigFile(join(fixtures, 'valid', 'full-stack.yml')) + const result = validate(config) + expect(result.errors).toHaveLength(0) + }) + + it('passes all monitor types', () => { + const config = parseConfigFile(join(fixtures, 'edge', 'all-monitor-types.yml')) + const result = validate(config) + expect(result.errors).toHaveLength(0) + }) + + it('passes all channel types', () => { + const config = parseConfigFile(join(fixtures, 'edge', 'all-channel-types.yml')) + const result = validate(config) + expect(result.errors).toHaveLength(0) + }) + }) + + describe('invalid configs', () => { + it('errors on missing monitor name', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'missing-name.yml')) + const result = validate(config) + expect(result.errors.length).toBeGreaterThan(0) + expect(result.errors.some((e) => e.path.includes('name'))).toBe(true) + }) + + it('errors on bad frequency', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'bad-frequency.yml')) + const result = validate(config) + expect(result.errors.length).toBeGreaterThan(0) + expect(result.errors.some((e) => e.message.includes('Frequency'))).toBe(true) + }) + + it('errors on invalid monitor type', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'bad-type.yml')) + const result = validate(config) + expect(result.errors.length).toBeGreaterThan(0) + expect(result.errors.some((e) => e.message.includes('Invalid type'))).toBe(true) + }) + + it('errors on invalid channel type', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'bad-channel-type.yml')) + const result = validate(config) + expect(result.errors.length).toBeGreaterThan(0) + expect(result.errors.some((e) => e.message.includes('Invalid channel type'))).toBe(true) + }) + + it('errors on duplicate names', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'duplicate-names.yml')) + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Duplicate'))).toBe(true) + }) + + it('errors on empty config', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'empty.yml')) + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('no resource'))).toBe(true) + }) + + it('errors on empty escalation steps', () => { + const config = parseConfigFile(join(fixtures, 'invalid', 'bad-escalation.yml')) + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('at least one step'))).toBe(true) + }) + }) + + describe('monitor config validation', () => { + it('errors when HTTP monitor missing url', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'HTTP', config: {method: 'GET'}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('url'))).toBe(true) + }) + + it('errors when DNS monitor missing hostname', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'DNS', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('hostname'))).toBe(true) + }) + + it('errors when TCP monitor missing host', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'TCP', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('host'))).toBe(true) + }) + + it('errors when ICMP monitor missing host', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'ICMP', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('host'))).toBe(true) + }) + + it('errors when HEARTBEAT monitor missing expectedInterval', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'HEARTBEAT', config: {gracePeriod: 60}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('expectedInterval'))).toBe(true) + }) + + it('errors when MCP_SERVER monitor missing command', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'MCP_SERVER', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('command'))).toBe(true) + }) + + it('errors on invalid HTTP method', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'HTTP', config: {url: 'https://x.com', method: 'INVALID'}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('HTTP method'))).toBe(true) + }) + + it('errors on invalid TCP port', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'TCP', config: {host: 'x.com', port: 99999}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('port'))).toBe(true) + }) + + it('errors when TCP port is 0', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'TCP', config: {host: 'x.com', port: 0}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('port'))).toBe(true) + }) + + it('errors when HEARTBEAT gracePeriod missing', () => { + const config: DevhelmConfig = { + monitors: [{name: 'test', type: 'HEARTBEAT', config: {expectedInterval: 60}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('gracePeriod'))).toBe(true) + }) + + it('errors on frequency above MAX_FREQUENCY (86400)', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 't', + type: 'HTTP', + config: {url: 'u', method: 'GET'}, + frequency: 100_000, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Frequency'))).toBe(true) + }) + + it('errors on invalid DNS recordType', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 't', + type: 'DNS', + config: {hostname: 'x', recordTypes: ['INVALID']}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('DNS record type'))).toBe(true) + }) + + it('errors on missing monitor type', () => { + const config: DevhelmConfig = { + monitors: [{name: 't', config: {url: 'x'}} as DevhelmConfig['monitors'] extends (infer M)[] | undefined ? M : never], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('type'))).toBe(true) + }) + + it('errors when monitor regions is not an array', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 't', + type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + regions: 'us-east' as unknown as string[], + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('array'))).toBe(true) + }) + }) + + describe('channel config validation', () => { + it('errors when slack missing webhookUrl', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'slack', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('webhookUrl'))).toBe(true) + }) + + it('errors when email missing recipients', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'email', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('recipients'))).toBe(true) + }) + + it('errors when pagerduty missing routingKey', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'pagerduty', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('routingKey'))).toBe(true) + }) + + it('errors when opsgenie missing apiKey', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'opsgenie', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('apiKey'))).toBe(true) + }) + + it('errors when webhook missing url', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'webhook', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('url'))).toBe(true) + }) + + it('errors when discord missing webhookUrl', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'discord', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('webhookUrl'))).toBe(true) + }) + + it('errors when teams missing webhookUrl', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'teams', config: {}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('webhookUrl'))).toBe(true) + }) + + it('errors when email recipients is empty array', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'test', type: 'email', config: {recipients: []}}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('recipients'))).toBe(true) + }) + }) + + describe('webhook definition validation', () => { + it('errors when events is empty', () => { + const config: DevhelmConfig = { + webhooks: [{url: 'https://x.com', events: []}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('events'))).toBe(true) + }) + + it('errors when url is missing', () => { + const config: DevhelmConfig = { + webhooks: [{url: '', events: ['monitor.down']}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('url'))).toBe(true) + }) + }) + + describe('resource group validation', () => { + it('errors on invalid healthThresholdType', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', healthThresholdType: 'INVALID' as string}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Must be one of'))).toBe(true) + }) + + it('validates defaultFrequency bounds', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', defaultFrequency: 1}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Frequency'))).toBe(true) + }) + + it('warns on unresolved alertPolicy reference', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', alertPolicy: 'nonexistent'}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('nonexistent'))).toBe(true) + }) + + it('warns on unresolved defaultEnvironment reference', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', defaultEnvironment: 'missing-env'}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('missing-env'))).toBe(true) + }) + + it('warns on unresolved defaultAlertChannels reference', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', defaultAlertChannels: ['missing-chan']}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('missing-chan'))).toBe(true) + }) + }) + + describe('dependency validation', () => { + it('errors on invalid alertSensitivity', () => { + const config: DevhelmConfig = { + dependencies: [{service: 'github', alertSensitivity: 'WRONG' as string}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Must be one of'))).toBe(true) + }) + + it('errors on missing service name', () => { + const config: DevhelmConfig = { + dependencies: [{service: ''}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('service'))).toBe(true) + }) + }) + + describe('auth validation', () => { + it('errors on invalid auth type', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'InvalidAuth' as string, secret: 'key'}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Auth type'))).toBe(true) + }) + + it('errors when auth missing secret', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'BearerAuthConfig', secret: ''}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('secret'))).toBe(true) + }) + + it('errors when ApiKeyAuthConfig missing headerName', () => { + const config: DevhelmConfig = { + secrets: [{key: 'my-key', value: 'val'}], + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'ApiKeyAuthConfig', secret: 'my-key'}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('headerName'))).toBe(true) + }) + + it('errors when HeaderAuthConfig missing headerName', () => { + const config: DevhelmConfig = { + secrets: [{key: 'my-key', value: 'val'}], + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'HeaderAuthConfig', secret: 'my-key'}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('headerName'))).toBe(true) + }) + + it('passes when ApiKeyAuthConfig has headerName', () => { + const config: DevhelmConfig = { + secrets: [{key: 'my-key', value: 'val'}], + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'ApiKeyAuthConfig', secret: 'my-key', headerName: 'X-API-Key'}, + }], + } + const result = validate(config) + const authErrors = result.errors.filter((e) => e.path.includes('auth')) + expect(authErrors).toHaveLength(0) + }) + + it('warns when auth secret not declared in YAML', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + auth: {type: 'BearerAuthConfig', secret: 'undeclared-secret'}, + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('undeclared-secret'))).toBe(true) + }) + }) + + describe('cross-reference warnings', () => { + it('warns on unresolved tag reference', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + tags: ['nonexistent-tag'], + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('nonexistent-tag'))).toBe(true) + }) + + it('warns on unresolved environment reference', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + environment: 'unknown-env', + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('unknown-env'))).toBe(true) + }) + + it('no warning when tag is declared in same config', () => { + const config: DevhelmConfig = { + tags: [{name: 'production'}], + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + tags: ['production'], + }], + } + const result = validate(config) + const tagWarnings = result.warnings.filter((w) => w.message.includes('production')) + expect(tagWarnings).toHaveLength(0) + }) + + it('warns on unresolved resource group monitor reference', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'group1', monitors: ['missing-monitor']}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('missing-monitor'))).toBe(true) + }) + + it('warns on unresolved alertChannel reference in monitor', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + alertChannels: ['missing-channel'], + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('missing-channel'))).toBe(true) + }) + + it('warns on unresolved resourceGroup reference in monitor', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + resourceGroup: 'nonexistent-group', + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('nonexistent-group'))).toBe(true) + }) + + it('warns on unresolved service ref in resource group', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'test', services: ['unknown-service']}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('unknown-service'))).toBe(true) + }) + }) + + describe('incident policy validation', () => { + it('errors on missing trigger rules', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: {triggerRules: [], confirmation: {type: 'multi_region'}, recovery: {}}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('trigger rule'))).toBe(true) + }) + + it('errors on invalid trigger type', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'invalid' as string, scope: 'per_region', severity: 'down'}], + confirmation: {type: 'multi_region'}, + recovery: {}, + }, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('trigger type'))).toBe(true) + }) + + it('errors on invalid trigger scope', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', scope: 'bad_scope' as string, severity: 'down'}], + confirmation: {type: 'multi_region'}, + recovery: {}, + }, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('scope'))).toBe(true) + }) + + it('errors on invalid trigger severity', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', scope: 'per_region', severity: 'bad' as string}], + confirmation: {type: 'multi_region'}, + recovery: {}, + }, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Must be one of'))).toBe(true) + }) + + it('errors on invalid aggregationType', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{ + type: 'consecutive_failures', + scope: 'per_region', + severity: 'down', + aggregationType: 'bad' as string, + }], + confirmation: {type: 'multi_region'}, + recovery: {}, + }, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Must be one of'))).toBe(true) + }) + + it('errors on missing confirmation in incident policy', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', scope: 'per_region', severity: 'down'}], + recovery: {}, + } as DevhelmConfig['monitors'] extends (infer M)[] | undefined ? NonNullable['incidentPolicy'] : never, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('confirmation'))).toBe(true) + }) + + it('errors on wrong confirmation type', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', scope: 'per_region', severity: 'down'}], + confirmation: {type: 'wrong_type' as 'multi_region'}, + recovery: {}, + }, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('Confirmation type'))).toBe(true) + }) + + it('errors on missing recovery in incident policy', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + incidentPolicy: { + triggerRules: [{type: 'consecutive_failures', scope: 'per_region', severity: 'down'}], + confirmation: {type: 'multi_region'}, + } as DevhelmConfig['monitors'] extends (infer M)[] | undefined ? NonNullable['incidentPolicy'] : never, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('recovery'))).toBe(true) + }) + }) + + describe('notification policy validation', () => { + it('errors on missing escalation', () => { + const config: DevhelmConfig = { + notificationPolicies: [{name: 'test'} as DevhelmConfig['notificationPolicies'] extends (infer T)[] | undefined ? T : never], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('escalation'))).toBe(true) + }) + + it('errors on negative priority', () => { + const config: DevhelmConfig = { + notificationPolicies: [{ + name: 'test', priority: -1, + escalation: {steps: [{channels: ['chan'], delayMinutes: 0}]}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('non-negative'))).toBe(true) + }) + + it('warns on unresolved channel in escalation step', () => { + const config: DevhelmConfig = { + notificationPolicies: [{ + name: 'test', + escalation: {steps: [{channels: ['nonexistent-channel']}]}, + }], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('nonexistent-channel'))).toBe(true) + }) + + it('errors on negative delayMinutes', () => { + const config: DevhelmConfig = { + alertChannels: [{name: 'ch', type: 'slack', config: {webhookUrl: 'url'}}], + notificationPolicies: [{ + name: 'test', + escalation: {steps: [{channels: ['ch'], delayMinutes: -5}]}, + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('delayMinutes'))).toBe(true) + }) + }) + + describe('environment validation', () => { + it('errors on invalid slug', () => { + const config: DevhelmConfig = { + environments: [{name: 'Prod', slug: 'Bad Slug!'}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('lowercase'))).toBe(true) + }) + + it('errors on missing environment name', () => { + const config: DevhelmConfig = { + environments: [{name: '', slug: 'prod'}], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('name'))).toBe(true) + }) + }) + + describe('tag validation', () => { + it('warns on bad color hex', () => { + const config: DevhelmConfig = { + tags: [{name: 'test', color: 'red'}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('hex'))).toBe(true) + }) + + it('no warning on valid hex color', () => { + const config: DevhelmConfig = { + tags: [{name: 'test', color: '#FF0000'}], + } + const result = validate(config) + const colorWarnings = result.warnings.filter((w) => w.path.includes('color')) + expect(colorWarnings).toHaveLength(0) + }) + }) + + describe('assertion validation', () => { + it('errors on invalid assertion type', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + assertions: [{type: 'InvalidAssertion', severity: 'error'}], + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('assertion type'))).toBe(true) + }) + + it('errors on invalid assertion severity', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + assertions: [{type: 'StatusCodeAssertion', severity: 'bad' as string}], + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('severity'))).toBe(true) + }) + + it('errors on invalid operator in assertion config', () => { + const config: DevhelmConfig = { + monitors: [{ + name: 'test', type: 'HTTP', + config: {url: 'https://x.com', method: 'GET'}, + assertions: [{type: 'StatusCodeAssertion', severity: 'error', config: {operator: 'INVALID_OP'}}], + }], + } + const result = validate(config) + expect(result.errors.some((e) => e.message.includes('operator'))).toBe(true) + }) + }) + + describe('version validation', () => { + it('warns on unknown config version', () => { + const config: DevhelmConfig = { + version: '99', + monitors: [{name: 'test', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + } + const result = validate(config) + expect(result.warnings.some((w) => w.message.includes('Unknown config version'))).toBe(true) + }) + + it('no warning on version 1', () => { + const config: DevhelmConfig = { + version: '1', + monitors: [{name: 'test', type: 'HTTP', config: {url: 'https://x.com', method: 'GET'}}], + } + const result = validate(config) + const versionWarnings = result.warnings.filter((w) => w.path === 'version') + expect(versionWarnings).toHaveLength(0) + }) + }) +}) From da3cf9a9ea041ee8556d20e4bdb4e598bcc8b41f Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 14:13:51 +0200 Subject: [PATCH 2/7] refactor: derive snapshot types from OpenAPI UpdateRequest schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all hand-written *Snapshot interfaces with types derived from the generated OpenAPI Update*Request schemas via Required. This guarantees that when the API contract changes (field added, removed, or renamed), the TypeScript compiler immediately errors in the snapshot functions, preventing silent drift between YAML engine and API. Key changes: - Remove dead alwaysChanged plumbing from HandlerDef / defineHandler - Replace 14 custom snapshot interfaces with Required type aliases (tag, environment, notificationPolicy, webhook, resourceGroup, monitor) or documented custom types for special cases (secret: hash-based, alertChannel: hash-based, dependency: split API) - Remove 7 old normalization helpers, replace with 4 API-type-aligned helpers (sortAssertions, apiAssertionsToSnapshot, apiIncidentPolicyToSnapshot, apiTagsToSnapshot) - Export toCreateAssertionRequest and toIncidentPolicy from transform.ts - Centralize as-any casts into api-client.ts helper functions - Add typed RefEntry generics to resolver.ts - Add RefTypeDtoMap to types.ts for compile-time DTO mapping - Fix webhook snapshot to also track enabled field - Fix resourceGroup snapshot field name (defaultAlertChannelIds → defaultAlertChannels) to match API contract - Make toDesiredSnapshot / toCurrentSnapshot required (not optional) Made-with: Cursor --- docs/openapi/monitoring-api.json | 16673 ++++++++++++++++++- src/commands/alert-channels/test.ts | 4 +- src/commands/api-keys/revoke.ts | 4 +- src/commands/auth/login.ts | 9 +- src/commands/auth/me.ts | 20 +- src/commands/data/services/status.ts | 4 +- src/commands/data/services/uptime.ts | 6 +- src/commands/dependencies/track.ts | 6 +- src/commands/deploy.ts | 90 +- src/commands/incidents/delete.ts | 4 - src/commands/incidents/resolve.ts | 6 +- src/commands/incidents/update.ts | 4 - src/commands/monitors/pause.ts | 4 +- src/commands/monitors/results.ts | 26 +- src/commands/monitors/resume.ts | 4 +- src/commands/monitors/test.ts | 4 +- src/commands/notification-policies/test.ts | 4 +- src/commands/status.ts | 7 +- src/commands/webhooks/test.ts | 4 +- src/lib/api-client.ts | 31 + src/lib/api.generated.ts | 934 +- src/lib/crud-commands.ts | 24 +- src/lib/resource-types.ts | 94 + src/lib/resources.ts | 30 +- src/lib/typed-api.ts | 47 +- src/lib/yaml/applier.ts | 7 +- src/lib/yaml/entitlements.ts | 45 +- src/lib/yaml/handlers.ts | 390 +- src/lib/yaml/resolver.ts | 27 +- src/lib/yaml/transform.ts | 6 +- src/lib/yaml/types.ts | 16 + test/yaml/applier.test.ts | 139 +- test/yaml/differ.test.ts | 4 +- test/yaml/entitlements.test.ts | 43 +- test/yaml/hash-contract.test.ts | 89 + test/yaml/hashing.test.ts | 120 + test/yaml/idempotency.test.ts | 265 + test/yaml/parity.test.ts | 83 +- test/yaml/transform.test.ts | 2 +- 39 files changed, 18258 insertions(+), 1021 deletions(-) delete mode 100644 src/commands/incidents/delete.ts delete mode 100644 src/commands/incidents/update.ts create mode 100644 src/lib/resource-types.ts create mode 100644 test/yaml/hash-contract.test.ts create mode 100644 test/yaml/hashing.test.ts create mode 100644 test/yaml/idempotency.test.ts diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index b35ca5d..db390c8 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -1 +1,16672 @@ -{"openapi":"3.0.1","info":{"title":"DevHelm API","description":"DevHelm platform and public API","version":"1.0"},"servers":[{"url":"http://localhost:8081","description":"Generated server url"}],"tags":[{"name":"Heartbeat","description":"Public ping endpoint for heartbeat monitors"},{"name":"Invites","description":"Organization invite management"},{"name":"Onboarding","description":"User onboarding flow"},{"name":"Members","description":"Organization member management"},{"name":"Me","description":"Current user profile and organizations"},{"name":"Incidents","description":"Incident management and lifecycle"},{"name":"Maintenance Windows","description":"Schedule alert-suppression windows for monitors"},{"name":"Organizations","description":"Organization management"},{"name":"Integrations","description":"Static catalog of supported alert channel integrations"},{"name":"Incident Policies","description":"Manage trigger, confirmation, and recovery rules for monitors"},{"name":"Entitlements","description":"Plan entitlements and usage limits"},{"name":"Vault","description":"Organization vault management (admin-only)"},{"name":"Secrets","description":"Organization environment secret management"},{"name":"Transactions","description":"Subscription transaction history"},{"name":"Monitors","description":"Monitor CRUD and lifecycle management"},{"name":"Webhooks","description":"Webhook endpoint management, event catalog, and delivery history"},{"name":"Events","description":"Real-time event stream"},{"name":"Workspaces","description":"Workspace management within an organization"},{"name":"Notifications","description":"In-app notification center"},{"name":"Alert Channels","description":"Alert channel CRUD and connectivity testing"},{"name":"Subscriptions","description":"Organization subscription management"},{"name":"Service Subscriptions","description":"Manage which services an organization tracks"},{"name":"Tags","description":"Org-scoped tag management for monitors"},{"name":"Status Data","description":"Public service status catalog, components, uptime, and incident history"},{"name":"Check Results","description":"Query raw check results, uptime statistics, and summary data"},{"name":"API Keys","description":"Organization API key management"},{"name":"Dashboard","description":"Overview dashboard aggregates"},{"name":"Auth","description":"User registration"},{"name":"Monitor Auth","description":"Manage authentication configuration for a monitor"},{"name":"Audit Log","description":"Organization audit trail"},{"name":"Monitor Alert Channels","description":"Manage alert channel mappings for a monitor"},{"name":"Alert Deliveries","description":"Delivery audit trail: inspect per-attempt details for alert deliveries"},{"name":"Resource Groups","description":"Resource group CRUD and member management"},{"name":"Notification Policies","description":"Org-level notification routing policies with JSONB match rules"},{"name":"Notification Dispatches","description":"Dispatch debugging API: inspect which policies matched an incident and track delivery status"},{"name":"Environments","description":"Variable namespace management for monitors"},{"name":"Monitor Assertions","description":"Manage assertions for a monitor"},{"name":"Billing","description":"Billing plans and pricing"}],"paths":{"/platform/orgs/{orgId}/subscriptions/{subscriptionId}":{"put":{"tags":["Subscriptions"],"summary":"Update subscription","operationId":"updateSubscription","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSubscriptionDto"}}}}}}},"/platform/onboarding/orgs/{orgId}/details":{"put":{"tags":["Onboarding"],"summary":"Update organization details","operationId":"updateOrgDetails","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/onboarding/advance":{"put":{"tags":["Onboarding"],"summary":"Advance onboarding stage forward","operationId":"advanceStage","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOnboardingStageRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/me":{"get":{"tags":["Me"],"summary":"Get current user","operationId":"me","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}},"put":{"tags":["Me"],"summary":"Update current user profile","operationId":"updateProfile","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/me/notification-preferences":{"get":{"tags":["Me"],"summary":"Get current user's notification preferences","operationId":"getNotificationPreferences","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPreferencesDto"}}}}}},"put":{"tags":["Me"],"summary":"Update current user's notification preferences","operationId":"updateNotificationPreferences","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPreferencesDto"}}}}}}},"/platform/admin/workspaces/{workspaceId}":{"get":{"tags":["admin-workspace-controller"],"operationId":"getWorkspace","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"put":{"tags":["admin-workspace-controller"],"operationId":"updateWorkspace","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"delete":{"tags":["admin-workspace-controller"],"operationId":"deleteWorkspace","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/admin/users/{userId}":{"put":{"tags":["admin-controller"],"operationId":"updateUser","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/admin/orgs/{orgId}":{"put":{"tags":["admin-controller"],"operationId":"updateOrganization","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/admin/orgs/{orgId}/members/{userId}/role":{"put":{"tags":["admin-member-controller"],"operationId":"updateMemberRole","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/workspaces/{workspaceId}":{"get":{"tags":["Workspaces"],"summary":"Get workspace by ID","operationId":"get","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"put":{"tags":["Workspaces"],"summary":"Update workspace","operationId":"update","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"delete":{"tags":["Workspaces"],"summary":"Delete workspace","operationId":"delete","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a single webhook endpoint","operationId":"get_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}},"put":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","operationId":"update_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","operationId":"delete_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/tags/{id}":{"put":{"tags":["Tags"],"summary":"Update a tag's name and/or color","operationId":"update_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTagRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTagDto"}}}}}},"delete":{"tags":["Tags"],"summary":"Delete a tag (cascades to all monitor associations)","operationId":"delete_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/secrets/{key}":{"put":{"tags":["Secrets"],"summary":"Update secret","operationId":"update_3","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSecretRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSecretDto"}}}}}},"delete":{"tags":["Secrets"],"summary":"Delete secret","operationId":"delete_3","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}":{"get":{"tags":["Resource Groups"],"summary":"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.","operationId":"get_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"includeMetrics","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}},"put":{"tags":["Resource Groups"],"summary":"Update a resource group's name, description, alert policy, inherited settings, and health threshold","operationId":"update_4","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceGroupRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}},"delete":{"tags":["Resource Groups"],"summary":"Delete a resource group (cascades to member rows)","operationId":"delete_4","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/org":{"get":{"tags":["Organizations"],"summary":"Get the current organization","operationId":"get_3","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}},"put":{"tags":["Organizations"],"summary":"Update the current organization","operationId":"update_5","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/api/v1/notifications/{id}/read":{"put":{"tags":["Notifications"],"summary":"Mark a notification as read","operationId":"markRead","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/notifications/read-all":{"put":{"tags":["Notifications"],"summary":"Mark all notifications as read","operationId":"markAllRead","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/notification-policies/{id}":{"get":{"tags":["Notification Policies"],"summary":"Get a notification policy by ID","operationId":"getById","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}},"put":{"tags":["Notification Policies"],"summary":"Update a notification policy","operationId":"update_6","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPolicyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}},"delete":{"tags":["Notification Policies"],"summary":"Delete a notification policy","operationId":"delete_5","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/policy":{"get":{"tags":["Incident Policies"],"summary":"Get incident policy for a monitor","description":"Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.","operationId":"get_4","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","description":"Monitor UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Policy found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IncidentPolicyDto"}}}},"404":{"description":"Monitor or policy not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}},"put":{"tags":["Incident Policies"],"summary":"Update incident policy for a monitor","description":"Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.","operationId":"update_7","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","description":"Monitor UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"}}},"required":true},"responses":{"200":{"description":"Policy updated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IncidentPolicyDto"}}}},"400":{"description":"Validation error in JSONB shape","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}},"404":{"description":"Monitor or policy not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}}},"/api/v1/monitors/{monitorId}/auth":{"put":{"tags":["Monitor Auth"],"summary":"Update authentication config for a monitor","operationId":"update_8","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMonitorAuthRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}},"post":{"tags":["Monitor Auth"],"summary":"Set authentication config for a monitor","operationId":"set","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMonitorAuthRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}},"delete":{"tags":["Monitor Auth"],"summary":"Remove authentication config from a monitor","operationId":"remove","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/assertions/{assertionId}":{"put":{"tags":["Monitor Assertions"],"summary":"Update an assertion on a monitor","operationId":"update_9","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"assertionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssertionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAssertionDto"}}}}}},"delete":{"tags":["Monitor Assertions"],"summary":"Remove an assertion from a monitor","operationId":"remove_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"assertionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/alert-channels":{"put":{"tags":["Monitor Alert Channels"],"summary":"Replace the linked alert channel set for a monitor","operationId":"setChannels","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAlertChannelsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListUUID"}}}}}}},"/api/v1/monitors/{id}":{"get":{"tags":["Monitors"],"summary":"Get a single monitor by id","operationId":"get_5","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}},"put":{"tags":["Monitors"],"summary":"Update a monitor","operationId":"update_10","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMonitorRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}},"delete":{"tags":["Monitors"],"summary":"Soft-delete a monitor","operationId":"delete_6","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}/status":{"put":{"tags":["Members"],"summary":"Change member status","operationId":"changeStatus","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeStatusRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}/role":{"put":{"tags":["Members"],"summary":"Change member role","operationId":"changeRole","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/maintenance-windows/{id}":{"get":{"tags":["Maintenance Windows"],"summary":"Get a single maintenance window by ID","operationId":"getById_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}},"put":{"tags":["Maintenance Windows"],"summary":"Update a maintenance window","operationId":"update_11","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaintenanceWindowRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}},"delete":{"tags":["Maintenance Windows"],"summary":"Delete a maintenance window","operationId":"delete_7","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/environments/{slug}":{"get":{"tags":["Environments"],"summary":"Get environment by slug","operationId":"get_6","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}},"put":{"tags":["Environments"],"summary":"Update environment","operationId":"update_12","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}},"delete":{"tags":["Environments"],"summary":"Delete environment","operationId":"delete_8","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/alert-channels/{id}":{"put":{"tags":["Alert Channels"],"summary":"Update an alert channel's name and re-encrypt config","operationId":"update_13","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlertChannelRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertChannelDto"}}}}}},"delete":{"tags":["Alert Channels"],"summary":"Soft-delete an alert channel and return affected policy summary","operationId":"delete_9","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DeleteChannelResult"}}}}}}},"/v1/webhooks/paddle":{"post":{"tags":["paddle-webhook-controller"],"operationId":"handleWebhook","parameters":[{"name":"paddle-signature","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/v1/internal/workspaces":{"post":{"tags":["workspaces-controller"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceCreateParams"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/v1/internal/service-incidents":{"post":{"tags":["service-incident-internal-controller"],"operationId":"createOrResolve","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceIncidentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}}},"/v1/internal/resource-groups/services/{serviceId}/re-evaluate-health":{"post":{"tags":["resource-groups-internal-controller"],"operationId":"reEvaluateGroupHealthForService","parameters":[{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/resource-groups/monitors/{monitorId}/re-evaluate-health":{"post":{"tags":["resource-groups-internal-controller"],"operationId":"reEvaluateGroupHealthForMonitor","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/incidents":{"post":{"tags":["incidents-internal-controller"],"operationId":"createAutoIncident","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAutoIncidentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/incidents/{id}/resolve":{"post":{"tags":["incidents-internal-controller"],"operationId":"resolveAutoIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/incidents/{id}/reopen":{"post":{"tags":["incidents-internal-controller"],"operationId":"reopenAutoIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReopenAutoIncidentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/escalation-tick":{"post":{"tags":["escalation-internal-controller"],"operationId":"runEscalationTick","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/billing/sync":{"post":{"tags":["admin-billing-controller"],"operationId":"syncFromPaddle","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/adapters/health":{"get":{"tags":["adapter-health-internal-controller"],"operationId":"getAllHealth","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAdapterHealthDto"}}}}}},"post":{"tags":["adapter-health-internal-controller"],"operationId":"reportOutcome","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdapterHealthReportRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdapterHealthDto"}}}}}}},"/platform/orgs":{"post":{"tags":["Organizations"],"summary":"Create organization","operationId":"create_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"ifNotExists","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrgRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/orgs/{orgId}/transactions":{"get":{"tags":["Transactions"],"summary":"List transactions","operationId":"list","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"limit","in":"query","required":false,"schema":{"maximum":100,"minimum":1,"type":"integer","format":"int32","default":10}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTransactionDto"}}}}}},"post":{"tags":["Transactions"],"summary":"Create subscription transaction","operationId":"createSubscriptionTransaction","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTransactionDto"}}}}}}},"/platform/onboarding/quick-monitor":{"post":{"tags":["Onboarding"],"summary":"Create a monitor with smart defaults from URL analysis","operationId":"quickMonitor","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickMonitorRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/platform/onboarding/complete-setup":{"post":{"tags":["Onboarding"],"summary":"Complete onboarding setup (creates org + workspace, advances to FIRST_MONITOR)","operationId":"completeSetup","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingSetupRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/onboarding/analyze-url":{"post":{"tags":["Onboarding"],"summary":"Analyze a URL and return suggested monitor configuration","operationId":"analyzeUrl","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeUrlRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAnalyzeUrlResponse"}}}}}}},"/platform/invites/accept":{"post":{"tags":["Invites"],"summary":"Accept invite","operationId":"accept","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAcceptInviteDto"}}}}}}},"/platform/auth/register":{"post":{"tags":["Auth"],"summary":"Register user","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterUserRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/admin/orgs/{orgId}/workspaces":{"get":{"tags":["admin-workspace-controller"],"operationId":"listWorkspaces","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}},"post":{"tags":["admin-workspace-controller"],"operationId":"createWorkspace","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/platform/admin/orgs/{orgId}/members":{"get":{"tags":["admin-member-controller"],"operationId":"listMembers","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMemberDto"}}}}}},"post":{"tags":["admin-member-controller"],"operationId":"addMember","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMemberRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMemberDto"}}}}}}},"/platform/admin/adapters/{serviceId}/enable":{"post":{"tags":["admin-adapter-health-controller"],"operationId":"reEnableAdapter","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdapterHealthDto"}}}}}}},"/api/v1/workspaces":{"get":{"tags":["Workspaces"],"summary":"List workspaces","operationId":"list_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}},"post":{"tags":["Workspaces"],"summary":"Create workspace","operationId":"create_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints for the authenticated org","operationId":"list_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWebhookEndpointDto"}}}}}},"post":{"tags":["Webhooks"],"summary":"Register a new webhook endpoint","operationId":"create_3","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookEndpointRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}}},"/api/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test delivery to a webhook endpoint","operationId":"test","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWebhookEndpointRequest"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookTestResult"}}}}}}},"/api/v1/webhooks/signing-secret/rotate":{"post":{"tags":["Webhooks"],"summary":"Generate or rotate the organization webhook signing secret","operationId":"rotateSigningSecret","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseString"}}}}}}},"/api/v1/vaults/rotate":{"post":{"tags":["Vault"],"summary":"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.","operationId":"rotateDek","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDekRotationResultDto"}}}}}}},"/api/v1/tags":{"get":{"tags":["Tags"],"summary":"List tags for the authenticated organization","operationId":"list_3","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"post":{"tags":["Tags"],"summary":"Create a new tag","operationId":"create_4","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTagRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTagDto"}}}}}}},"/api/v1/service-subscriptions/{slug}":{"post":{"tags":["Service Subscriptions"],"summary":"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.","operationId":"subscribe","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceSubscribeRequest"}}}},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}}},"/api/v1/secrets":{"get":{"tags":["Secrets"],"summary":"List secrets","operationId":"list_4","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultSecretDto"}}}}}},"post":{"tags":["Secrets"],"summary":"Create secret","operationId":"create_5","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSecretRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSecretDto"}}}}}}},"/api/v1/resource-groups":{"get":{"tags":["Resource Groups"],"summary":"List all resource groups for the authenticated org with health summaries","operationId":"list_5","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultResourceGroupDto"}}}}}},"post":{"tags":["Resource Groups"],"summary":"Create a new resource group","operationId":"create_6","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceGroupRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}}},"/api/v1/resource-groups/{id}/members":{"post":{"tags":["Resource Groups"],"summary":"Add a monitor or service member to a resource group","operationId":"addMember_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddResourceGroupMemberRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupMemberDto"}}}}}}},"/api/v1/notification-policies":{"get":{"tags":["Notification Policies"],"summary":"List all notification policies for the authenticated org","operationId":"list_6","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationPolicyDto"}}}}}},"post":{"tags":["Notification Policies"],"summary":"Create a notification policy with match rules and escalation chain","operationId":"create_7","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotificationPolicyRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}}},"/api/v1/notification-policies/{id}/test":{"post":{"tags":["Notification Policies"],"summary":"Dry-run: evaluate a policy's match rules against a supplied incident context","operationId":"test_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestNotificationPolicyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestMatchResult"}}}}}}},"/api/v1/notification-dispatches/{id}/acknowledge":{"post":{"tags":["Notification Dispatches"],"summary":"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).","operationId":"acknowledge","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationDispatchDto"}}}}}}},"/api/v1/monitors":{"get":{"tags":["Monitors"],"summary":"List monitors for the authenticated org","operationId":"list_7","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"enabled","in":"query","description":"Filter by enabled state","required":false,"schema":{"type":"boolean"}},{"name":"type","in":"query","description":"Filter by monitor type","required":false,"schema":{"type":"string","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]}},{"name":"managedBy","in":"query","description":"Filter by managed-by source","required":false,"schema":{"type":"string","enum":["DASHBOARD","CLI"]}},{"name":"tags","in":"query","description":"Filter by tag names, comma-separated (e.g. prod,critical)","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive name search","required":false,"schema":{"type":"string"}},{"name":"environmentId","in":"query","description":"Filter by environment ID","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMonitorDto"}}}}}},"post":{"tags":["Monitors"],"summary":"Create a new monitor","operationId":"create_8","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMonitorRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{monitorId}/assertions":{"post":{"tags":["Monitor Assertions"],"summary":"Add an assertion to a monitor","operationId":"add","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAssertionRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAssertionDto"}}}}}}},"/api/v1/monitors/{id}/test":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"testExisting","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorTestResultDto"}}}}}}},"/api/v1/monitors/{id}/tags":{"get":{"tags":["Monitors"],"summary":"Get all tags applied to a monitor","operationId":"getMonitorTags","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"post":{"tags":["Monitors"],"summary":"Add tags to a monitor; supports existing tag IDs and inline creation of new tags","operationId":"addMonitorTags","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"delete":{"tags":["Monitors"],"summary":"Remove tags from a monitor by their IDs","operationId":"removeMonitorTags","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveMonitorTagsRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{id}/rotate-token":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"rotateToken","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{id}/resume":{"post":{"tags":["Monitors"],"summary":"Resume a monitor (set enabled=true)","operationId":"resume","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{id}/pause":{"post":{"tags":["Monitors"],"summary":"Pause a monitor (set enabled=false)","operationId":"pause","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/test":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"testAdHoc","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorTestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorTestResultDto"}}}}}}},"/api/v1/monitors/bulk":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"bulkAction","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkMonitorActionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseBulkMonitorActionResult"}}}}}}},"/api/v1/maintenance-windows":{"get":{"tags":["Maintenance Windows"],"summary":"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).","operationId":"list_8","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"monitorId","in":"query","description":"Filter by monitor UUID","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"filter","in":"query","description":"Filter by status: 'active' or 'upcoming'","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMaintenanceWindowDto"}}}}}},"post":{"tags":["Maintenance Windows"],"summary":"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.","operationId":"create_9","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaintenanceWindowRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}}},"/api/v1/invites":{"get":{"tags":["Invites"],"summary":"List invites","operationId":"list_9","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultInviteDto"}}}}}},"post":{"tags":["Invites"],"summary":"Create invite","operationId":"create_10","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInviteRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInviteDto"}}}}}}},"/api/v1/invites/{inviteId}/revoke":{"post":{"tags":["Invites"],"summary":"Revoke invite","operationId":"revoke","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"inviteId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/invites/{inviteId}/resend":{"post":{"tags":["Invites"],"summary":"Resend invite","operationId":"resend","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"inviteId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInviteDto"}}}}}}},"/api/v1/incidents":{"get":{"tags":["Incidents"],"summary":"List incidents for the authenticated org","operationId":"list_10","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"params","in":"query","required":true,"schema":{"$ref":"#/components/schemas/IncidentFilterParams"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}},"post":{"tags":["Incidents"],"summary":"Create a manual incident","operationId":"create_11","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateManualIncidentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/incidents/{id}/updates":{"post":{"tags":["Incidents"],"summary":"Add an update to an incident (optionally change status)","operationId":"addUpdate","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddIncidentUpdateRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/incidents/{id}/resolve":{"post":{"tags":["Incidents"],"summary":"Resolve an incident","operationId":"resolve","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveIncidentRequest"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/heartbeat/{token}":{"get":{"tags":["Heartbeat"],"summary":"Record a heartbeat ping (GET)","description":"Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.","operationId":"pingGet","parameters":[{"name":"token","in":"path","description":"Ping endpoint token for the heartbeat monitor","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}},"post":{"tags":["Heartbeat"],"summary":"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.","operationId":"pingPost","parameters":[{"name":"token","in":"path","description":"Ping endpoint token for the heartbeat monitor","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}},"*/*":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}}},"/api/v1/environments":{"get":{"tags":["Environments"],"summary":"List environments","operationId":"list_11","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultEnvironmentDto"}}}}}},"post":{"tags":["Environments"],"summary":"Create environment","operationId":"create_12","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}}},"/api/v1/api-keys":{"get":{"tags":["API Keys"],"summary":"List API keys","operationId":"list_12","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultApiKeyDto"}}}}}},"post":{"tags":["API Keys"],"summary":"Create API key","operationId":"create_13","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyCreateResponse"}}}}}}},"/api/v1/api-keys/{id}/revoke":{"post":{"tags":["API Keys"],"summary":"Revoke API key","operationId":"revoke_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyDto"}}}}}}},"/api/v1/api-keys/{id}/regenerate":{"post":{"tags":["API Keys"],"summary":"Regenerate API key","operationId":"regenerate","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyCreateResponse"}}}}}}},"/api/v1/alert-deliveries/{id}/retry":{"post":{"tags":["Alert Deliveries"],"summary":"Retry a failed delivery","description":"Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.","operationId":"retry","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertDeliveryDto"}}}}}}},"/api/v1/alert-channels":{"get":{"tags":["Alert Channels"],"summary":"List active alert channels for the authenticated org","operationId":"list_13","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAlertChannelDto"}}}}}},"post":{"tags":["Alert Channels"],"summary":"Create a new alert channel with encrypted config","operationId":"create_14","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlertChannelRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertChannelDto"}}}}}}},"/api/v1/alert-channels/{id}/test":{"post":{"tags":["Alert Channels"],"summary":"Test a saved alert channel's connectivity","operationId":"test_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestChannelResult"}}}}}}},"/api/v1/alert-channels/test":{"post":{"tags":["Alert Channels"],"summary":"Test alert channel connectivity using raw config (no saved channel required)","operationId":"testConfig","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAlertChannelRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestChannelResult"}}}}}}},"/v1/internal/service-incidents/by-ref/{serviceId}/{externalRef}/components":{"patch":{"tags":["service-incident-internal-controller"],"operationId":"addComponents","parameters":[{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"externalRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComponentUpdateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}}},"/api/v1/service-subscriptions/{id}/alert-sensitivity":{"patch":{"tags":["Service Subscriptions"],"summary":"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).","operationId":"updateAlertSensitivity","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlertSensitivityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}}},"/api/v1/api-keys/{id}":{"delete":{"tags":["API Keys"],"summary":"Delete API key","operationId":"delete_10","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}},"patch":{"tags":["API Keys"],"summary":"Update API key","operationId":"update_14","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKeyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyDto"}}}}}}},"/v1/internal/workspaces/{id}":{"get":{"tags":["workspaces-controller"],"operationId":"get_7","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/v1/internal/orgs/{id}/workspaces":{"get":{"tags":["orgs-controller"],"operationId":"listWorkspaces_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}}},"/v1/internal/monitors/{id}/policy":{"get":{"tags":["monitors-internal-controller"],"operationId":"policy","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}}},"/v1/internal/monitors/{id}/env-variables":{"get":{"tags":["monitors-internal-controller"],"operationId":"getEnvVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMapStringString"}}}}}}},"/v1/internal/monitors/{id}/auth":{"get":{"tags":["monitors-internal-controller"],"operationId":"auth","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}}},"/v1/internal/monitors/{id}/assertions":{"get":{"tags":["monitors-internal-controller"],"operationId":"getAssertions","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListMonitorAssertionDto"}}}}}}},"/v1/internal/monitors/{id}/active-incident":{"get":{"tags":["monitors-internal-controller"],"operationId":"activeIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/monitors/schedulable":{"get":{"tags":["monitors-internal-controller"],"operationId":"schedulable","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SchedulableMonitorDto"}}}}}}}},"/platform/plans":{"get":{"tags":["Billing"],"summary":"List public billing plans","operationId":"getPublicPlans","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListBillingPlanDto"}}}}}}},"/platform/orgs/{orgId}/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"List active subscriptions","operationId":"listActive","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultSubscriptionDto"}}}}}},"delete":{"tags":["Subscriptions"],"operationId":"cancel","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/orgs/{orgId}/subscriptions/upcoming-charge":{"get":{"tags":["Subscriptions"],"summary":"Get upcoming charge","operationId":"getUpcomingCharge","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"priceId","in":"query","required":true,"schema":{"minimum":1,"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUpcomingChargeResponse"}}}}}}},"/platform/orgs/{orgId}/subscriptions/management-urls":{"get":{"tags":["Subscriptions"],"summary":"Get subscription management URLs","operationId":"getManagementUrls","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMapStringString"}}}}}}},"/platform/orgs/{orgId}/subscriptions/customer-auth-token":{"get":{"tags":["Subscriptions"],"summary":"Get customer auth token","operationId":"getCustomerAuthToken","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseString"}}}}}}},"/platform/orgs/{orgId}/entitlements":{"get":{"tags":["Entitlements"],"summary":"Get resolved entitlements and current usage for the organization","operationId":"getEntitlements","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEntitlementResponse"}}}}}}},"/platform/orgs/search":{"get":{"tags":["Organizations"],"summary":"Search organizations","operationId":"searchOrganizations","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"query","in":"query","required":true,"schema":{"type":"string"}},{"name":"paginationParams","in":"query","required":true,"schema":{"$ref":"#/components/schemas/PaginationParams"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIdValuePair"}}}}}}},"/platform/me/orgs":{"get":{"tags":["Me"],"summary":"Get current user's organizations","operationId":"myOrgs","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMyOrgItemDto"}}}}}}},"/platform/events/stream":{"get":{"tags":["Events"],"summary":"Subscribe to real-time platform events via SSE","operationId":"stream","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/platform/admin/users":{"get":{"tags":["admin-controller"],"operationId":"listUsers","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultUserDto"}}}}}}},"/platform/admin/stats":{"get":{"tags":["admin-controller"],"operationId":"getStats","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdminStatsDto"}}}}}}},"/platform/admin/orgs":{"get":{"tags":["admin-controller"],"operationId":"listOrgs","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultOrganizationDto"}}}}}}},"/platform/admin/adapters/health":{"get":{"tags":["admin-adapter-health-controller"],"operationId":"getAdapterHealth","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAdapterHealthDto"}}}}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List recent deliveries for a webhook endpoint","operationId":"listDeliveries","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWebhookDeliveryDto"}}}}}}},"/api/v1/webhooks/signing-secret":{"get":{"tags":["Webhooks"],"summary":"Get signing secret metadata for the authenticated org","operationId":"getSigningSecretInfo","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookSigningSecretDto"}}}}}}},"/api/v1/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"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.","operationId":"listEvents","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/WebhookEventCatalogResponse"}}}}}}},"/api/v1/services":{"get":{"tags":["Status Data"],"summary":"List all enabled services (cursor-paginated)","operationId":"listServices","parameters":[{"name":"category","in":"query","description":"Filter by category (exact match)","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by current overall_status (exact match)","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Page size (1–100, default 20)","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageServiceCatalogDto"}}}}}}},"/api/v1/services/{slugOrId}":{"get":{"tags":["Status Data"],"summary":"Get a single service by slug or UUID with current status, components, and recent incidents","operationId":"getService","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceDetailDto"}}}}}}},"/api/v1/services/{slugOrId}/uptime":{"get":{"tags":["Status Data"],"summary":"Get uptime statistics for a service","description":"Uptime data aggregated across active non-group components.","operationId":"getServiceUptime","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"period","in":"query","description":"Time window","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","2y","all"]}},{"name":"granularity","in":"query","description":"Bucket granularity","required":false,"schema":{"type":"string","enum":["hourly","daily","monthly"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceUptimeResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/api/v1/services/{slugOrId}/maintenances":{"get":{"tags":["Status Data"],"summary":"List scheduled maintenances for a service","operationId":"getScheduledMaintenances","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by status (e.g. scheduled, in_progress, verifying, completed)","required":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultScheduledMaintenanceDto"}}}}}}},"/api/v1/services/{slugOrId}/incidents":{"get":{"tags":["Status Data"],"summary":"List incident history for a service (paginated)","operationId":"listIncidents","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","description":"Earliest start date (ISO 8601 date)","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","description":"Filter: active (unresolved), resolved, or omit for all","required":false,"schema":{"type":"string","enum":["active","resolved"]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceIncidentDto"}}}}}}},"/api/v1/services/{slugOrId}/incidents/{incidentId}":{"get":{"tags":["Status Data"],"summary":"Get incident detail with full update timeline","operationId":"getIncident","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"incidentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceIncidentDetailDto"}}}}}}},"/api/v1/services/{slugOrId}/components":{"get":{"tags":["Status Data"],"summary":"List active components for a service with current status and inline uptime","operationId":"getComponents","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceComponentDto"}}}}}}},"/api/v1/services/{slugOrId}/components/{componentId}/uptime":{"get":{"tags":["Status Data"],"summary":"Get daily uptime data for a component","operationId":"getComponentUptime","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"componentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"period","in":"query","description":"Time window","required":false,"schema":{"type":"string","enum":["7d","30d","90d","1y"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultComponentUptimeDayDto"}}}}}}},"/api/v1/services/summary":{"get":{"tags":["Status Data"],"summary":"Global status summary across all services","description":"Returns aggregate counts of services by status and a list of services currently experiencing issues.","operationId":"getGlobalStatusSummary","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseGlobalStatusSummaryDto"}}}}}}},"/api/v1/services/incidents":{"get":{"tags":["Status Data"],"summary":"List vendor incidents across all services (paginated)","description":"Cross-service vendor incident feed ordered by start date descending.","operationId":"listCrossServiceIncidents","parameters":[{"name":"from","in":"query","description":"Earliest start date (ISO 8601 date)","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","description":"Filter: active (unresolved), resolved, or omit for all","required":false,"schema":{"type":"string","enum":["active","resolved"]}},{"name":"category","in":"query","description":"Filter by service category","required":false,"schema":{"type":"string"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceIncidentDto"}}}}}}},"/api/v1/service-subscriptions":{"get":{"tags":["Service Subscriptions"],"summary":"List all service subscriptions for the organization","operationId":"list_14","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceSubscriptionDto"}}}}}}},"/api/v1/service-subscriptions/{id}":{"get":{"tags":["Service Subscriptions"],"summary":"Get a subscription by its ID","operationId":"get_8","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}},"delete":{"tags":["Service Subscriptions"],"summary":"Remove a subscription by its ID","description":"Removes a specific subscription (whole-service or component-level). No-op if not found.","operationId":"unsubscribe","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}/health":{"get":{"tags":["Resource Groups"],"summary":"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.","operationId":"getHealth","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupHealthDto"}}}}}}},"/api/v1/notifications":{"get":{"tags":["Notifications"],"summary":"List notifications for the current user","operationId":"list_15","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"unreadOnly","in":"query","required":false,"schema":{"type":"boolean","default":false}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDto"}}}}}}},"/api/v1/notifications/unread-count":{"get":{"tags":["Notifications"],"summary":"Get unread notification count","operationId":"unreadCount","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseLong"}}}}}}},"/api/v1/notification-policies/{id}/dispatches":{"get":{"tags":["Notification Policies"],"summary":"List all dispatches (firing history) for a notification policy","operationId":"listDispatches","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDispatchDto"}}}}}}},"/api/v1/notification-dispatches":{"get":{"tags":["Notification Dispatches"],"summary":"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.","operationId":"listByIncident","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"incident_id","in":"query","description":"UUID of the incident to inspect","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDispatchDto"}}}}}}},"/api/v1/notification-dispatches/{id}":{"get":{"tags":["Notification Dispatches"],"summary":"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.","operationId":"getById_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationDispatchDto"}}}}}}},"/api/v1/monitors/{id}/versions":{"get":{"tags":["Monitors"],"summary":"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.","operationId":"listVersions","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMonitorVersionDto"}}}}}}},"/api/v1/monitors/{id}/versions/{version}":{"get":{"tags":["Monitors"],"summary":"Get a specific version snapshot for a monitor","description":"Returns the full monitor config snapshot captured at the given version number.","operationId":"getVersion","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"version","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorVersionDto"}}}}}}},"/api/v1/monitors/{id}/uptime":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getUptime","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Time window for uptime calculation","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d"]}}],"responses":{"200":{"description":"Uptime statistics","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UptimeDto"}}}},"400":{"description":"Invalid window parameter","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}}}}},"/api/v1/monitors/{id}/results":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getResults","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","description":"Start of time range (ISO 8601, inclusive); defaults to 24 hours ago","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of time range (ISO 8601, inclusive); defaults to now","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response for pagination","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Maximum results per page (1–200)","required":false,"schema":{"type":"integer","format":"int32","default":50},"example":50},{"name":"region","in":"query","description":"Filter by region (e.g. us-east)","required":false,"schema":{"type":"string"}},{"name":"passed","in":"query","description":"Filter by pass/fail status","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated check results","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPage"}}}},"400":{"description":"Invalid query parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}}}}},"/api/v1/monitors/{id}/results/summary":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getSummary","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"chartWindow","in":"query","description":"Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d"]}}],"responses":{"200":{"description":"Results summary","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ResultSummaryDto"}}}},"400":{"description":"Invalid chartWindow parameter","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}}}}},"/api/v1/members":{"get":{"tags":["Members"],"summary":"List organization members","operationId":"list_16","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMemberDto"}}}}}}},"/api/v1/integrations":{"get":{"tags":["Integrations"],"summary":"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.","operationId":"list_17","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IntegrationCatalogResponse"}}}}}}},"/api/v1/incidents/{id}":{"get":{"tags":["Incidents"],"summary":"Get incident details including update timeline","operationId":"get_9","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/dashboard/overview":{"get":{"tags":["Dashboard"],"summary":"Dashboard overview","description":"Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.","operationId":"overview","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDashboardOverviewDto"}}}}}}},"/api/v1/categories":{"get":{"tags":["Status Data"],"summary":"List categories with service counts","operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultCategoryDto"}}}}}}},"/api/v1/audit-log":{"get":{"tags":["Audit Log"],"summary":"List audit events for the current organization","operationId":"list_18","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"action","in":"query","required":false,"schema":{"type":"string"}},{"name":"actorId","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"resourceType","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PageResultAuditEventDto"}}}}}}},"/api/v1/alert-deliveries/{id}/attempts":{"get":{"tags":["Alert Deliveries"],"summary":"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.","operationId":"listAttempts","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultDeliveryAttemptDto"}}}}}}},"/api/v1/alert-channels/{id}/deliveries":{"get":{"tags":["Alert Channels"],"summary":"List delivery history for an alert channel","operationId":"listDeliveries_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAlertDeliveryDto"}}}}}}},"/platform/orgs/{orgId}":{"delete":{"tags":["Organizations"],"summary":"Delete organization","operationId":"delete_11","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/admin/orgs/{orgId}/members/{userId}":{"delete":{"tags":["admin-member-controller"],"operationId":"removeMember","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}/members/{memberId}":{"delete":{"tags":["Resource Groups"],"summary":"Remove a member from a resource group","operationId":"removeMember_1","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"memberId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}":{"delete":{"tags":["Members"],"summary":"Remove member from organization","operationId":"remove_2","parameters":[{"name":"actor","in":"query","required":true,"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiKey"},{"$ref":"#/components/schemas/Internal"},{"$ref":"#/components/schemas/UI"}]}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}}},"components":{"schemas":{"Actor":{"type":"object"},"ApiKey":{"type":"object","allOf":[{"$ref":"#/components/schemas/Actor"},{"type":"object","properties":{"orgId":{"type":"integer","format":"int32"},"keyId":{"type":"integer","format":"int32"}}}]},"Internal":{"type":"object","allOf":[{"$ref":"#/components/schemas/Actor"}]},"OrgContext":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"role":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]}}},"UI":{"type":"object","allOf":[{"$ref":"#/components/schemas/Actor"},{"type":"object","properties":{"userContext":{"$ref":"#/components/schemas/UserContext"},"orgContext":{"$ref":"#/components/schemas/OrgContext"},"workspaceId":{"type":"integer","format":"int32","nullable":true}}}]},"UserContext":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"role":{"type":"string","enum":["SUPERADMIN","ADMIN","USER"]}}},"CreateSubscriptionRequest":{"type":"object","properties":{"priceId":{"minimum":1,"type":"integer","format":"int32"}}},"BillingPlanDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"paddleId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"prices":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/BillingPriceDto"}}},"nullable":true},"BillingPriceDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"paddleId":{"type":"string"},"amount":{"type":"integer","format":"int32"},"interval":{"type":"string","enum":["DAY","WEEK","MONTH","YEAR"]},"intervalCount":{"type":"integer","format":"int32"},"description":{"type":"string","nullable":true},"billingPlan":{"$ref":"#/components/schemas/BillingPlanDto"}}},"ItemDto":{"type":"object","properties":{"billingPrice":{"$ref":"#/components/schemas/BillingPriceDto"},"quantity":{"type":"integer","format":"int32"},"amount":{"type":"integer","format":"int32"}}},"SingleValueResponseSubscriptionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SubscriptionDto"}}},"SubscriptionDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"paddleId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"organizationId":{"type":"integer","format":"int32"},"status":{"type":"string","enum":["ACTIVE","CANCELED","PAST_DUE","PAUSED","TRIALING"]},"nextBilledAt":{"type":"string","format":"date-time","nullable":true},"willCancelAt":{"type":"string","format":"date-time","nullable":true},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemDto"}}}},"UpdateOrgDetailsRequest":{"required":["email","name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string"},"email":{"minLength":1,"type":"string","format":"email"},"size":{"maxLength":50,"minLength":0,"type":"string"},"industry":{"maxLength":100,"minLength":0,"type":"string"},"websiteUrl":{"maxLength":255,"minLength":0,"type":"string"}}},"OrganizationDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"size":{"type":"string","nullable":true},"industry":{"type":"string","nullable":true},"websiteUrl":{"type":"string","nullable":true}}},"SingleValueResponseOrganizationDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrganizationDto"}}},"UpdateOnboardingStageRequest":{"required":["stage"],"type":"object","properties":{"stage":{"type":"string","enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]}}},"SingleValueResponseUserDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UserDto"}}},"UserDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"email":{"type":"string"},"emailVerified":{"type":"boolean"},"name":{"type":"string","nullable":true},"userRole":{"type":"string","enum":["SUPERADMIN","ADMIN","USER"]},"onboardingStage":{"type":"string","nullable":true,"enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]},"imageUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateProfileRequest":{"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string"}}},"UpdateNotificationPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"type":"object","additionalProperties":{"type":"boolean"}}}},"NotificationPreferencesDto":{"type":"object","properties":{"preferences":{"type":"object","additionalProperties":{"type":"boolean"}},"updatedAt":{"type":"string","format":"date-time"}}},"SingleValueResponseNotificationPreferencesDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationPreferencesDto"}}},"UpdateWorkspaceRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string"}}},"SingleValueResponseWorkspaceDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WorkspaceDto"}}},"WorkspaceDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"orgId":{"type":"integer","format":"int32"}}},"UpdateUserRequest":{"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string"},"email":{"type":"string","format":"email"},"userRole":{"type":"string","enum":["SUPERADMIN","ADMIN","USER"]},"onboardingStage":{"type":"string","enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]},"imageUrl":{"maxLength":500,"minLength":0,"type":"string"}}},"ChangeRoleRequest":{"required":["orgRole"],"type":"object","properties":{"orgRole":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]}}},"UpdateWebhookEndpointRequest":{"type":"object","properties":{"url":{"maxLength":2048,"minLength":0,"type":"string","description":"New webhook URL; null preserves current","nullable":true},"description":{"maxLength":255,"minLength":0,"type":"string","description":"New description; null preserves current","nullable":true},"subscribedEvents":{"type":"array","description":"Replace subscribed events; null preserves current","nullable":true,"items":{"type":"string","description":"Replace subscribed events; null preserves current","nullable":true}},"enabled":{"type":"boolean","description":"Enable or disable delivery; null preserves current","nullable":true}}},"SingleValueResponseWebhookEndpointDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookEndpointDto"}}},"WebhookEndpointDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"description":{"type":"string","nullable":true},"subscribedEvents":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"consecutiveFailures":{"type":"integer","format":"int32"},"disabledReason":{"type":"string","nullable":true},"disabledAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateTagRequest":{"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"New tag name","nullable":true},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"New hex color code","nullable":true}},"description":"Request body for updating a tag; null fields are left unchanged"},"SingleValueResponseTagDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TagDto"}}},"TagDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"integer","format":"int32"},"name":{"type":"string"},"color":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"UpdateSecretRequest":{"required":["value"],"type":"object","properties":{"value":{"maxLength":32768,"minLength":0,"type":"string","description":"New secret value, stored encrypted (max 32KB)"}}},"MonitorReference":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"SecretDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"key":{"type":"string"},"dekVersion":{"type":"integer","format":"int32"},"valueHash":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"usedByMonitors":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/MonitorReference"}}}},"SingleValueResponseSecretDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SecretDto"}}},"RetryStrategy":{"required":["type"],"type":"object","properties":{"type":{"type":"string"},"maxRetries":{"type":"integer","format":"int32"},"interval":{"type":"integer","format":"int32"}},"description":"Default retry strategy for member monitors; null clears"},"UpdateResourceGroupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this group"},"description":{"type":"string","description":"Optional description; null clears the existing value","nullable":true},"alertPolicyId":{"type":"string","description":"Optional notification policy to apply for this group; null clears the existing value","format":"uuid","nullable":true},"defaultFrequency":{"maximum":86400,"minimum":30,"type":"integer","description":"Default check frequency in seconds for members (30–86400); null clears","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions for member monitors; null clears","nullable":true,"items":{"type":"string","description":"Default regions for member monitors; null clears","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs for member monitors; null clears","nullable":true,"items":{"type":"string","description":"Default alert channel IDs for member monitors; null clears","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID for member monitors; null clears","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE; null disables threshold","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"maximum":100,"exclusiveMaximum":false,"minimum":0,"exclusiveMinimum":false,"type":"number","description":"Health threshold value; null disables threshold","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"Suppress member-level alert notifications; null preserves current value","nullable":true},"confirmationDelaySeconds":{"maximum":600,"minimum":0,"type":"integer","description":"Confirmation delay in seconds; null clears","format":"int32","nullable":true},"recoveryCooldownMinutes":{"maximum":60,"minimum":0,"type":"integer","description":"Recovery cooldown in minutes; null clears","format":"int32","nullable":true}},"description":"Request body for updating a resource group"},"ResourceGroupDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"integer","format":"int32"},"name":{"type":"string"},"slug":{"type":"string"},"description":{"type":"string","nullable":true},"alertPolicyId":{"type":"string","description":"Notification policy applied to this group","format":"uuid","nullable":true},"defaultFrequency":{"type":"integer","description":"Default check frequency in seconds for member monitors","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions for member monitors","nullable":true,"items":{"type":"string","description":"Default regions for member monitors","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs for member monitors","nullable":true,"items":{"type":"string","description":"Default alert channel IDs for member monitors","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID for member monitors","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"type":"number","description":"Health threshold value","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"When true, member-level incidents skip notification dispatch; only group alerts fire"},"confirmationDelaySeconds":{"type":"integer","description":"Seconds to wait after health threshold breach before creating group incident","format":"int32","nullable":true},"recoveryCooldownMinutes":{"type":"integer","description":"Cooldown minutes after group incident resolves before a new one can open","format":"int32","nullable":true},"health":{"$ref":"#/components/schemas/ResourceGroupHealthDto"},"members":{"type":"array","description":"Member list with individual statuses; populated on detail GET only","nullable":true,"items":{"$ref":"#/components/schemas/ResourceGroupMemberDto"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"description":"Resource group with health summary and optional member details"},"ResourceGroupHealthDto":{"type":"object","properties":{"status":{"type":"string","description":"Worst-of health status across all members","enum":["operational","maintenance","degraded","down"]},"totalMembers":{"type":"integer","description":"Total number of members in the group","format":"int32"},"operationalCount":{"type":"integer","description":"Number of members currently in operational status","format":"int32"},"activeIncidents":{"type":"integer","description":"Number of members with an active incident or non-operational status","format":"int32"},"thresholdStatus":{"type":"string","description":"Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured.","nullable":true,"enum":["healthy","degraded","down"]},"failingCount":{"type":"integer","description":"Number of failing members at time of last evaluation","format":"int32","nullable":true}},"description":"Aggregated health summary for a resource group"},"ResourceGroupMemberDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"groupId":{"type":"string","format":"uuid"},"memberType":{"type":"string","description":"Type of member: 'monitor' or 'service'"},"monitorId":{"type":"string","description":"Monitor ID; set when memberType is 'monitor'","format":"uuid","nullable":true},"serviceId":{"type":"string","description":"Service ID; set when memberType is 'service'","format":"uuid","nullable":true},"name":{"type":"string","description":"Display name of the referenced monitor or service","nullable":true},"slug":{"type":"string","description":"Slug identifier for the service (services only); used for icons and uptime API calls","nullable":true},"subscriptionId":{"type":"string","description":"Subscription ID for the service (services only); used to link to the dependency detail page","format":"uuid","nullable":true},"status":{"type":"string","description":"Computed health status for this member","enum":["operational","maintenance","degraded","down"]},"effectiveFrequency":{"type":"string","description":"Effective check frequency label showing the group default when the monitor inherits it; null for services or when no group default is configured","nullable":true},"createdAt":{"type":"string","format":"date-time"},"uptime24h":{"type":"number","description":"24h uptime percentage; populated when includeMetrics=true","format":"double","nullable":true},"chartData":{"type":"array","description":"Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true","nullable":true,"items":{"type":"number","description":"Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true","format":"double","nullable":true}},"avgLatencyMs":{"type":"number","description":"Average latency in ms (monitors only); populated when includeMetrics=true","format":"double","nullable":true},"p95LatencyMs":{"type":"number","description":"P95 latency in ms (monitors only); populated when includeMetrics=true","format":"double","nullable":true},"lastCheckedAt":{"type":"string","description":"Timestamp of the most recent health check; populated when includeMetrics=true","format":"date-time","nullable":true},"monitorType":{"type":"string","description":"Monitor type (HTTP, DNS, TCP, ICMP, HEARTBEAT, MCP); monitors only","nullable":true},"environmentName":{"type":"string","description":"Environment name; monitors only","nullable":true}},"description":"A single member of a resource group with its computed health status"},"SingleValueResponseResourceGroupDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupDto"}}},"EscalationChain":{"required":["steps"],"type":"object","properties":{"steps":{"minItems":1,"type":"array","description":"Ordered escalation steps, evaluated in sequence","items":{"$ref":"#/components/schemas/EscalationStep"}},"onResolve":{"type":"string","description":"Action when the incident resolves","nullable":true},"onReopen":{"type":"string","description":"Action when a resolved incident reopens","nullable":true}},"description":"Escalation chain defining which channels to notify"},"EscalationStep":{"required":["channelIds"],"type":"object","properties":{"delayMinutes":{"minimum":0,"type":"integer","description":"Minutes to wait before executing this step (0 = immediate)","format":"int32"},"channelIds":{"minItems":1,"type":"array","description":"Alert channel IDs to notify in this step","items":{"type":"string","description":"Alert channel IDs to notify in this step","format":"uuid"}},"requireAck":{"type":"boolean","description":"Whether an acknowledgment is required before escalating","nullable":true},"repeatIntervalSeconds":{"minimum":1,"type":"integer","description":"Repeat notification interval in seconds until acknowledged","format":"int32","nullable":true}},"description":"Ordered escalation steps, evaluated in sequence"},"MatchRule":{"required":["type"],"type":"object","properties":{"type":{"type":"string","description":"Rule type, e.g. severity_gte, monitor_id_in, region_in"},"value":{"type":"string","description":"Comparison value for single-value rules like severity_gte","nullable":true},"monitorIds":{"type":"array","description":"Monitor UUIDs to match for monitor_id_in rules","nullable":true,"items":{"type":"string","description":"Monitor UUIDs to match for monitor_id_in rules","format":"uuid","nullable":true}},"regions":{"type":"array","description":"Region codes to match for region_in rules","nullable":true,"items":{"type":"string","description":"Region codes to match for region_in rules","nullable":true}},"values":{"type":"array","description":"Values list for multi-value rules like monitor_type_in","nullable":true,"items":{"type":"string","description":"Values list for multi-value rules like monitor_type_in","nullable":true}}},"description":"Match rules to evaluate (all must pass; omit or empty for catch-all)"},"UpdateNotificationPolicyRequest":{"required":["enabled","escalation","name","priority"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this policy"},"matchRules":{"type":"array","description":"Match rules to evaluate (all must pass; omit or empty for catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is enabled"},"priority":{"type":"integer","description":"Evaluation priority; higher value = evaluated first","format":"int32"}},"description":"Request body for updating a notification policy"},"NotificationPolicyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"integer","format":"int32"},"name":{"type":"string","description":"Human-readable name for this policy"},"matchRules":{"type":"array","description":"Match rules (all must pass; empty = catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is active"},"priority":{"type":"integer","description":"Evaluation order; higher value = evaluated first","format":"int32"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"description":"Org-level notification policy with match rules and escalation chain"},"SingleValueResponseNotificationPolicyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationPolicyDto"}}},"ConfirmationPolicy":{"required":["type"],"type":"object","properties":{"type":{"type":"string","enum":["multi_region"]},"minRegionsFailing":{"type":"integer","format":"int32"},"maxWaitSeconds":{"type":"integer","format":"int32"}},"description":"Multi-region confirmation settings"},"IncidentPolicyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"triggerRules":{"type":"array","description":"Array of trigger rules defining when an incident should be raised","items":{"$ref":"#/components/schemas/TriggerRule"}},"confirmation":{"$ref":"#/components/schemas/ConfirmationPolicy"},"recovery":{"$ref":"#/components/schemas/RecoveryPolicy"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"monitorRegionCount":{"type":"integer","description":"Number of regions configured on the monitor (only set in internal API responses)","format":"int32","nullable":true},"checkFrequencySeconds":{"type":"integer","description":"Monitor check frequency in seconds (only set in internal API responses)","format":"int32","nullable":true}},"description":"Incident detection, confirmation, and recovery policy for a monitor"},"RecoveryPolicy":{"type":"object","properties":{"consecutiveSuccesses":{"type":"integer","format":"int32"},"minRegionsPassing":{"type":"integer","format":"int32"},"cooldownMinutes":{"type":"integer","format":"int32"}},"description":"Auto-recovery settings"},"TriggerRule":{"required":["scope","severity","type"],"type":"object","properties":{"type":{"type":"string","enum":["consecutive_failures","failures_in_window","response_time"]},"count":{"type":"integer","format":"int32","nullable":true},"windowMinutes":{"type":"integer","format":"int32","nullable":true},"scope":{"type":"string","nullable":true,"enum":["per_region","any_region"]},"thresholdMs":{"type":"integer","format":"int32","nullable":true},"severity":{"type":"string","enum":["down","degraded"]},"aggregationType":{"type":"string","nullable":true,"enum":["all_exceed","average","p95","max"]}},"description":"Array of trigger rules defining when an incident should be raised"},"UpdateIncidentPolicyRequest":{"required":["confirmation","recovery","triggerRules"],"type":"object","properties":{"triggerRules":{"minItems":1,"type":"array","description":"Array of trigger rules; at least one required","items":{"$ref":"#/components/schemas/TriggerRule"}},"confirmation":{"$ref":"#/components/schemas/ConfirmationPolicy"},"recovery":{"$ref":"#/components/schemas/RecoveryPolicy"}},"description":"Request body for updating an incident policy"},"SingleValueResponseIncidentPolicyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentPolicyDto"}}},"ApiKeyAuthConfig":{"required":["headerName"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"pattern":"^[A-Za-z0-9\\-_]+$","type":"string"},"vaultSecretId":{"type":"string","format":"uuid","nullable":true}}}]},"BasicAuthConfig":{"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"vaultSecretId":{"type":"string","format":"uuid","nullable":true}}}]},"BearerAuthConfig":{"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"vaultSecretId":{"type":"string","format":"uuid","nullable":true}}}]},"HeaderAuthConfig":{"required":["headerName"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"pattern":"^[A-Za-z0-9\\-_]+$","type":"string"},"vaultSecretId":{"type":"string","format":"uuid","nullable":true}}}]},"MonitorAuthConfig":{"required":["type"],"type":"object","properties":{"type":{"type":"string"}},"discriminator":{"propertyName":"type"}},"UpdateMonitorAuthRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"MonitorAuthDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"authType":{"type":"string","enum":["bearer","basic","header","api_key"]},"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"SingleValueResponseMonitorAuthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorAuthDto"}}},"AssertionConfig":{"required":["type"],"type":"object","properties":{"type":{"type":"string"}},"discriminator":{"propertyName":"type"}},"BodyContainsAssertion":{"required":["substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"substring":{"minLength":1,"type":"string"}}}]},"DnsExpectedCnameAssertion":{"required":["value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"value":{"minLength":1,"type":"string"}}}]},"DnsExpectedIpsAssertion":{"required":["ips"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"ips":{"minItems":1,"type":"array","items":{"type":"string"}}}}]},"DnsMaxAnswersAssertion":{"required":["recordType"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string"},"max":{"type":"integer","format":"int32"}}}]},"DnsMinAnswersAssertion":{"required":["recordType"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string"},"min":{"type":"integer","format":"int32"}}}]},"DnsRecordContainsAssertion":{"required":["recordType","substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string"},"substring":{"minLength":1,"type":"string"}}}]},"DnsRecordEqualsAssertion":{"required":["recordType","value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string"},"value":{"minLength":1,"type":"string"}}}]},"DnsResolvesAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"DnsResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","format":"int32"}}}]},"DnsResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","format":"int32"}}}]},"DnsTtlHighAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxTtl":{"type":"integer","format":"int32"}}}]},"DnsTtlLowAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"minTtl":{"type":"integer","format":"int32"}}}]},"DnsTxtContainsAssertion":{"required":["substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"substring":{"minLength":1,"type":"string"}}}]},"HeaderValueAssertion":{"required":["expected","headerName","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"type":"string"},"expected":{"minLength":1,"type":"string"},"operator":{"type":"string","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"HeartbeatIntervalDriftAssertion":{"required":["maxDeviationPercent"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxDeviationPercent":{"maximum":100,"minimum":1,"type":"integer","format":"int32"}}}]},"HeartbeatMaxIntervalAssertion":{"required":["maxSeconds"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxSeconds":{"minimum":1,"type":"integer","format":"int32"}}}]},"HeartbeatPayloadContainsAssertion":{"required":["path","value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"path":{"minLength":1,"type":"string"},"value":{"type":"string"}}}]},"HeartbeatReceivedAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"IcmpPacketLossAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxPercent":{"maximum":100.0,"exclusiveMaximum":false,"minimum":0.0,"exclusiveMinimum":false,"type":"number","format":"double"}}}]},"IcmpReachableAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"IcmpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","format":"int32"}}}]},"IcmpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","format":"int32"}}}]},"JsonPathAssertion":{"required":["expected","operator","path"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"path":{"minLength":1,"type":"string"},"expected":{"minLength":1,"type":"string"},"operator":{"type":"string","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"McpConnectsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"McpHasCapabilityAssertion":{"required":["capability"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"capability":{"minLength":1,"type":"string"}}}]},"McpMinToolsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"min":{"type":"integer","format":"int32"}}}]},"McpProtocolVersionAssertion":{"required":["version"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"version":{"minLength":1,"type":"string"}}}]},"McpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","format":"int32"}}}]},"McpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","format":"int32"}}}]},"McpToolAvailableAssertion":{"required":["toolName"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"toolName":{"minLength":1,"type":"string"}}}]},"McpToolCountChangedAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expectedCount":{"type":"integer","format":"int32"}}}]},"RedirectCountAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxCount":{"type":"integer","format":"int32"}}}]},"RedirectTargetAssertion":{"required":["expected","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expected":{"minLength":1,"type":"string"},"operator":{"type":"string","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"RegexBodyAssertion":{"required":["pattern"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"pattern":{"minLength":1,"type":"string"}}}]},"ResponseSizeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxBytes":{"type":"integer","format":"int32"}}}]},"ResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"thresholdMs":{"type":"integer","format":"int32"}}}]},"ResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","format":"int32"}}}]},"SslExpiryAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"minDaysRemaining":{"type":"integer","format":"int32"}}}]},"StatusCodeAssertion":{"required":["expected","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expected":{"minLength":1,"type":"string"},"operator":{"type":"string","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"TcpConnectsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"TcpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","format":"int32"}}}]},"TcpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","format":"int32"}}}]},"UpdateAssertionRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","enum":["fail","warn"]}}},"MonitorAssertionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"assertionType":{"type":"string","enum":["status_code","response_time","body_contains","json_path","header","regex","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":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","enum":["fail","warn"]}}},"SingleValueResponseMonitorAssertionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorAssertionDto"}}},"SetAlertChannelsRequest":{"required":["channelIds"],"type":"object","properties":{"channelIds":{"type":"array","items":{"type":"string","format":"uuid"}}}},"SingleValueResponseListUUID":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string","format":"uuid"}}}},"AddMonitorTagsRequest":{"type":"object","properties":{"tagIds":{"type":"array","description":"IDs of existing org tags to attach","nullable":true,"items":{"type":"string","description":"IDs of existing org tags to attach","format":"uuid","nullable":true}},"newTags":{"type":"array","description":"New tags to create (if not already present) and attach","nullable":true,"items":{"$ref":"#/components/schemas/NewTagRequest"}}},"description":"Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both."},"CreateAssertionRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","enum":["fail","warn"]}},"description":"Replace all assertions; null preserves current"},"DnsMonitorConfig":{"required":["hostname"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"hostname":{"minLength":1,"type":"string","description":"Domain name to resolve"},"recordTypes":{"type":"array","description":"DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR","nullable":true,"items":{"type":"string","description":"DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR","nullable":true,"enum":["A","AAAA","CNAME","MX","NS","TXT","SRV","SOA","CAA","PTR"]}},"nameservers":{"type":"array","description":"Custom nameservers to query (uses system defaults if omitted)","nullable":true,"items":{"type":"string","description":"Custom nameservers to query (uses system defaults if omitted)","nullable":true}},"timeoutMs":{"type":"integer","description":"Per-query timeout in milliseconds","format":"int32","nullable":true},"totalTimeoutMs":{"type":"integer","description":"Total timeout for all queries in milliseconds","format":"int32","nullable":true}}}]},"HeartbeatMonitorConfig":{"required":["expectedInterval","gracePeriod"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"expectedInterval":{"maximum":86400,"minimum":1,"type":"integer","description":"Expected heartbeat interval in seconds","format":"int32"},"gracePeriod":{"minimum":1,"type":"integer","description":"Grace period in seconds before marking as down","format":"int32"}}}]},"HttpMonitorConfig":{"required":["method","url"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Target URL to send requests to"},"method":{"type":"string","description":"HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD","enum":["GET","POST","PUT","PATCH","DELETE","HEAD"]},"customHeaders":{"type":"object","additionalProperties":{"type":"string","description":"Additional HTTP headers to include in requests","nullable":true},"description":"Additional HTTP headers to include in requests","nullable":true},"requestBody":{"type":"string","description":"Request body content for POST/PUT/PATCH methods","nullable":true},"contentType":{"type":"string","description":"Content-Type header value for the request body","nullable":true},"verifyTls":{"type":"boolean","description":"Whether to verify TLS certificates (default: true)","nullable":true}}}]},"IcmpMonitorConfig":{"required":["host"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"host":{"minLength":1,"type":"string","description":"Target hostname or IP address to ping"},"packetCount":{"maximum":20,"minimum":1,"type":"integer","description":"Number of ICMP packets to send","format":"int32","nullable":true},"timeoutMs":{"type":"integer","description":"Ping timeout in milliseconds","format":"int32","nullable":true}}}]},"McpServerMonitorConfig":{"required":["command"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"command":{"minLength":1,"type":"string","description":"Command to execute to start the MCP server"},"args":{"type":"array","description":"Command-line arguments for the MCP server process","nullable":true,"items":{"type":"string","description":"Command-line arguments for the MCP server process","nullable":true}},"env":{"type":"object","additionalProperties":{"type":"string","description":"Environment variables to pass to the MCP server process","nullable":true},"description":"Environment variables to pass to the MCP server process","nullable":true}}}]},"MonitorConfig":{"type":"object","description":"Updated protocol-specific configuration; null preserves current"},"NewTagRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Tag name"},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"Hex color code (defaults to #6B7280 if omitted)","nullable":true}},"description":"Inline tag creation — creates the tag if it does not already exist"},"TcpMonitorConfig":{"required":["host"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"host":{"minLength":1,"type":"string","description":"Target hostname or IP address"},"port":{"maximum":65535,"minimum":1,"type":"integer","description":"TCP port to connect to","format":"int32"},"timeoutMs":{"type":"integer","description":"Connection timeout in milliseconds","format":"int32","nullable":true}}}]},"UpdateMonitorRequest":{"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"New monitor name; null preserves current","nullable":true},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"New check frequency in seconds (30–86400); null preserves current","format":"int32","nullable":true},"enabled":{"type":"boolean","description":"Enable or disable the monitor; null preserves current","nullable":true},"regions":{"type":"array","description":"New probe regions; null preserves current","nullable":true,"items":{"type":"string","description":"New probe regions; null preserves current","nullable":true}},"managedBy":{"type":"string","description":"New management source; null preserves current","nullable":true,"enum":["DASHBOARD","CLI"]},"environmentId":{"type":"string","description":"New environment ID; null preserves current (use clearEnvironmentId to unset)","format":"uuid","nullable":true},"clearEnvironmentId":{"type":"boolean","description":"Set to true to remove the environment association","nullable":true},"assertions":{"type":"array","description":"Replace all assertions; null preserves current","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}},"auth":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]},"clearAuth":{"type":"boolean","description":"Set to true to remove authentication","nullable":true},"incidentPolicy":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"},"alertChannelIds":{"type":"array","description":"Replace alert channel list; null preserves current","nullable":true,"items":{"type":"string","description":"Replace alert channel list; null preserves current","format":"uuid","nullable":true}},"tags":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"MonitorDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"integer","format":"int32"},"name":{"type":"string"},"type":{"type":"string","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"regions":{"type":"array","items":{"type":"string"}},"managedBy":{"type":"string","enum":["DASHBOARD","CLI"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"assertions":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/MonitorAssertionDto"}},"tags":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/TagDto"}},"pingUrl":{"type":"string","nullable":true},"environment":{"$ref":"#/components/schemas/Summary"},"auth":{"$ref":"#/components/schemas/MonitorAuthDto"},"incidentPolicy":{"$ref":"#/components/schemas/IncidentPolicyDto"},"alertChannelIds":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid","nullable":true}}}},"SingleValueResponseMonitorDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorDto"}}},"Summary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"}}},"ChangeStatusRequest":{"required":["status"],"type":"object","properties":{"status":{"type":"string","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}}},"UpdateMaintenanceWindowRequest":{"required":["endsAt","startsAt"],"type":"object","properties":{"monitorId":{"type":"string","format":"uuid"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"repeatRule":{"maxLength":100,"minLength":0,"type":"string"},"reason":{"type":"string"},"suppressAlerts":{"type":"boolean"}}},"MaintenanceWindowDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid","nullable":true},"organizationId":{"type":"integer","format":"int32"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"repeatRule":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true},"suppressAlerts":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseMaintenanceWindowDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MaintenanceWindowDto"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"New environment name; null preserves current","nullable":true},"variables":{"type":"object","additionalProperties":{"type":"string","description":"Replace all variables; null preserves current","nullable":true},"description":"Replace all variables; null preserves current","nullable":true},"isDefault":{"type":"boolean","description":"Whether this is the default environment; null preserves current","nullable":true}}},"EnvironmentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"integer","format":"int32"},"name":{"type":"string"},"slug":{"type":"string"},"variables":{"type":"object","additionalProperties":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"monitorCount":{"type":"integer","format":"int32"},"isDefault":{"type":"boolean"}}},"SingleValueResponseEnvironmentDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EnvironmentDto"}}},"ChannelConfig":{"required":["channelType"],"type":"object","properties":{"channelType":{"type":"string"}},"description":"New channel configuration (full replacement, not partial update)","discriminator":{"propertyName":"channelType"}},"DiscordChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Discord webhook URL"},"mentionRoleId":{"type":"string","description":"Optional Discord role ID to mention in notifications","nullable":true}}}]},"EmailChannelConfig":{"required":["recipients"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"recipients":{"minItems":1,"type":"array","description":"Email addresses to send notifications to","items":{"type":"string","description":"Email addresses to send notifications to","format":"email"}}}}]},"OpsGenieChannelConfig":{"required":["apiKey"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"apiKey":{"minLength":1,"type":"string","description":"OpsGenie API key for alert creation"},"region":{"type":"string","description":"OpsGenie API region: us or eu","nullable":true}}}]},"PagerDutyChannelConfig":{"required":["routingKey"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"routingKey":{"minLength":1,"type":"string","description":"PagerDuty Events API v2 routing (integration) key"},"severityOverride":{"type":"string","description":"Override PagerDuty severity mapping","nullable":true}}}]},"SlackChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Slack incoming webhook URL"},"mentionText":{"type":"string","description":"Optional mention text included in notifications, e.g. @channel","nullable":true}}}]},"TeamsChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Microsoft Teams incoming webhook URL"}}}]},"UpdateAlertChannelRequest":{"required":["config","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"New channel name (full replacement, not partial update)"},"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}}},"WebhookChannelConfig":{"required":["url"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Webhook endpoint URL that receives alert payloads"},"signingSecret":{"type":"string","description":"Optional HMAC signing secret for payload verification","nullable":true},"customHeaders":{"type":"object","additionalProperties":{"type":"string","description":"Additional HTTP headers to include in webhook requests","nullable":true},"description":"Additional HTTP headers to include in webhook requests","nullable":true}}}]},"AlertChannelDto":{"required":["channelType","createdAt","id","name","updatedAt"],"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"channelType":{"type":"string","enum":["email","webhook","slack","pagerduty","opsgenie","teams","discord"]},"displayConfig":{"type":"object","additionalProperties":{"type":"object","nullable":true},"nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastDeliveryAt":{"type":"string","format":"date-time","nullable":true},"lastDeliveryStatus":{"type":"string","nullable":true}}},"SingleValueResponseAlertChannelDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AlertChannelDto"}}},"WorkspaceCreateParams":{"required":["name"],"type":"object","properties":{"organizationId":{"type":"integer","format":"int32"},"name":{"minLength":1,"type":"string"}}},"ServiceIncidentRequest":{"required":["action","externalRef","serviceId","title"],"type":"object","properties":{"serviceId":{"type":"string","format":"uuid"},"externalRef":{"minLength":1,"type":"string"},"severity":{"type":"string","nullable":true},"title":{"minLength":1,"type":"string"},"shortlink":{"type":"string","nullable":true},"affectedComponents":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"serviceIncidentId":{"type":"string","format":"uuid","nullable":true},"action":{"minLength":1,"type":"string"},"statusText":{"type":"string","nullable":true}}},"IncidentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid","nullable":true},"organizationId":{"type":"integer","format":"int32"},"source":{"type":"string","enum":["AUTOMATIC","MANUAL","MONITORS","STATUS_DATA","RESOURCE_GROUP"]},"status":{"type":"string","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"severity":{"type":"string","enum":["DOWN","DEGRADED","MAINTENANCE"]},"title":{"type":"string","nullable":true},"triggeredByRule":{"type":"string","nullable":true},"affectedRegions":{"type":"array","items":{"type":"string"}},"reopenCount":{"type":"integer","format":"int32"},"createdByUserId":{"type":"integer","format":"int32","nullable":true},"statusPageVisible":{"type":"boolean"},"serviceIncidentId":{"type":"string","format":"uuid","nullable":true},"serviceId":{"type":"string","format":"uuid","nullable":true},"externalRef":{"type":"string","nullable":true},"affectedComponents":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"shortlink":{"type":"string","nullable":true},"resolutionReason":{"type":"string","nullable":true,"enum":["MANUAL","AUTO_RECOVERED","AUTO_RESOLVED"]},"startedAt":{"type":"string","format":"date-time","nullable":true},"confirmedAt":{"type":"string","format":"date-time","nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"cooldownUntil":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"monitorName":{"type":"string","nullable":true},"serviceName":{"type":"string","nullable":true},"serviceSlug":{"type":"string","nullable":true},"monitorType":{"type":"string","nullable":true},"resourceGroupId":{"type":"string","format":"uuid","nullable":true},"resourceGroupName":{"type":"string","nullable":true}}},"TableValueResultIncidentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IncidentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseInteger":{"type":"object","properties":{"data":{"type":"integer","format":"int32"}}},"CreateAutoIncidentRequest":{"required":["monitorId"],"type":"object","properties":{"monitorId":{"type":"string","format":"uuid"},"severity":{"type":"string","nullable":true},"triggeredByRule":{"type":"string","nullable":true},"affectedRegions":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"startedAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseIncidentDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentDto"}}},"ReopenAutoIncidentRequest":{"type":"object","properties":{"affectedRegions":{"type":"array","items":{"type":"string"}},"severity":{"type":"string","nullable":true}}},"AdapterHealthReportRequest":{"required":["serviceId","success"],"type":"object","properties":{"serviceId":{"type":"string","format":"uuid"},"success":{"type":"boolean"},"errorMessage":{"type":"string","nullable":true}}},"AdapterHealthDto":{"type":"object","properties":{"serviceId":{"type":"string","format":"uuid"},"serviceSlug":{"type":"string"},"serviceName":{"type":"string"},"adapterType":{"type":"string","nullable":true},"lastSuccessAt":{"type":"string","format":"date-time","nullable":true},"lastFailureAt":{"type":"string","format":"date-time","nullable":true},"consecutiveFailures":{"type":"integer","format":"int32"},"lastErrorMessage":{"type":"string","nullable":true},"disabledByHealth":{"type":"boolean"},"updatedAt":{"type":"string","format":"date-time"}}},"SingleValueResponseAdapterHealthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdapterHealthDto"}}},"CreateOrgRequest":{"required":["name"],"type":"object","properties":{"name":{"minLength":1,"type":"string"},"email":{"type":"string","format":"email","nullable":true}}},"SingleValueResponseTransactionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TransactionDto"}}},"TransactionDto":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","nullable":true},"currencyCode":{"type":"string","nullable":true},"invoiceNumber":{"type":"string","nullable":true},"billedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"total":{"type":"string","nullable":true},"subtotal":{"type":"string","nullable":true},"tax":{"type":"string","nullable":true}}},"QuickMonitorRequest":{"required":["url"],"type":"object","properties":{"url":{"minLength":1,"type":"string"},"name":{"type":"string","nullable":true},"frequencySeconds":{"type":"integer","format":"int32","nullable":true}}},"OnboardingSetupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string"},"role":{"maxLength":50,"minLength":0,"type":"string","nullable":true},"teamSize":{"maxLength":50,"minLength":0,"type":"string","nullable":true}}},"AnalyzeUrlRequest":{"required":["url"],"type":"object","properties":{"url":{"minLength":1,"type":"string"}}},"AnalyzeUrlResponse":{"type":"object","properties":{"reachable":{"type":"boolean"},"responseTimeMs":{"type":"integer","format":"int64"},"statusCode":{"type":"integer","format":"int32"},"tlsExpiry":{"type":"string","format":"date-time","nullable":true},"tlsDaysRemaining":{"type":"integer","format":"int32","nullable":true},"contentType":{"type":"string","nullable":true},"suggestedName":{"type":"string"},"suggestedAssertions":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedAssertion"}},"suggestedFrequencySeconds":{"type":"integer","format":"int32"}}},"SingleValueResponseAnalyzeUrlResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AnalyzeUrlResponse"}}},"SuggestedAssertion":{"type":"object","properties":{"type":{"type":"string"},"operator":{"type":"string"},"value":{"type":"string"}}},"AcceptInviteRequest":{"required":["token"],"type":"object","properties":{"token":{"minLength":1,"type":"string"}}},"AcceptInviteDto":{"type":"object","properties":{"orgId":{"type":"integer","format":"int32"},"userId":{"type":"integer","format":"int32"},"orgRole":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}}},"SingleValueResponseAcceptInviteDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AcceptInviteDto"}}},"RegisterUserRequest":{"type":"object","properties":{"nickname":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"picture":{"type":"string","nullable":true}}},"CreateWorkspaceRequest":{"required":["name"],"type":"object","properties":{"name":{"minLength":1,"type":"string"}}},"AddMemberRequest":{"required":["orgRole","userId"],"type":"object","properties":{"userId":{"type":"integer","format":"int32"},"orgRole":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]}}},"MemberDto":{"type":"object","properties":{"userId":{"type":"integer","format":"int32"},"email":{"type":"string"},"name":{"type":"string","nullable":true},"orgRole":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseMemberDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MemberDto"}}},"CreateWebhookEndpointRequest":{"required":["subscribedEvents","url"],"type":"object","properties":{"url":{"maxLength":2048,"minLength":0,"type":"string","description":"HTTPS endpoint that receives webhook event payloads"},"description":{"maxLength":255,"minLength":0,"type":"string","description":"Optional human-readable description"},"subscribedEvents":{"minItems":1,"type":"array","description":"Event types to deliver, e.g. monitor.created, incident.resolved","items":{"minLength":1,"type":"string","description":"Event types to deliver, e.g. monitor.created, incident.resolved"}}}},"TestWebhookEndpointRequest":{"type":"object","properties":{"eventType":{"type":"string","nullable":true}}},"SingleValueResponseWebhookTestResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookTestResult"}}},"WebhookTestResult":{"type":"object","properties":{"success":{"type":"boolean"},"statusCode":{"type":"integer","format":"int32","nullable":true},"message":{"type":"string"},"durationMs":{"type":"integer","format":"int64","nullable":true}}},"SingleValueResponseString":{"type":"object","properties":{"data":{"type":"string"}}},"DekRotationResultDto":{"type":"object","properties":{"previousDekVersion":{"type":"integer","format":"int32"},"newDekVersion":{"type":"integer","format":"int32"},"secretsReEncrypted":{"type":"integer","format":"int32"},"channelsReEncrypted":{"type":"integer","format":"int32"},"rotatedAt":{"type":"string","format":"date-time"}}},"SingleValueResponseDekRotationResultDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DekRotationResultDto"}}},"CreateTagRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Tag name, unique within the org"},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"Hex color code (defaults to #6B7280 if omitted)","nullable":true}},"description":"Request body for creating a tag"},"ServiceSubscribeRequest":{"type":"object","properties":{"componentId":{"type":"string","description":"ID of the component to subscribe to. Omit or null for whole-service subscription.","format":"uuid","nullable":true},"alertSensitivity":{"type":"string","description":"Alert sensitivity level. Defaults to INCIDENTS_ONLY when not provided.","nullable":true}},"description":"Optional body for subscribing to a specific component of a service"},"ComponentUptimeSummaryDto":{"type":"object","properties":{"day":{"type":"number","description":"Uptime percentage over the last 24 hours","format":"double","nullable":true,"example":99.95},"week":{"type":"number","description":"Uptime percentage over the last 7 days","format":"double","nullable":true,"example":99.98},"month":{"type":"number","description":"Uptime percentage over the last 30 days","format":"double","nullable":true,"example":99.92},"source":{"type":"string","description":"Data source: vendor_reported or incident_derived","example":"vendor_reported"}},"description":"Inline uptime percentages for 24h, 7d, 30d"},"ServiceComponentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"description":{"type":"string","nullable":true},"groupId":{"type":"string","format":"uuid","nullable":true},"position":{"type":"integer","format":"int32","nullable":true},"showcase":{"type":"boolean"},"onlyShowIfDegraded":{"type":"boolean"},"startDate":{"type":"string","format":"date-time","nullable":true},"vendorCreatedAt":{"type":"string","format":"date-time","nullable":true},"lifecycleStatus":{"type":"string"},"dataType":{"type":"string","description":"Data classification: full, status_only, or metric_only","example":"full"},"hasUptime":{"type":"boolean","description":"Whether uptime data is available for this component"},"region":{"type":"string","description":"Geographic region for regional components (AWS, GCP, Azure)","nullable":true},"groupName":{"type":"string","description":"Display name of the parent group","nullable":true},"uptime":{"$ref":"#/components/schemas/ComponentUptimeSummaryDto"},"statusChangedAt":{"type":"string","format":"date-time","nullable":true},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"group":{"type":"boolean"}},"description":"A first-class service component with lifecycle and uptime data"},"ServiceSubscriptionDto":{"type":"object","properties":{"subscriptionId":{"type":"string","description":"Unique subscription identifier","format":"uuid"},"serviceId":{"type":"string","description":"Service identifier","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"logoUrl":{"type":"string","description":"Logo URL from the service catalog","nullable":true},"overallStatus":{"type":"string","description":"Current overall status; null when the service has never been polled","nullable":true},"componentId":{"type":"string","description":"Subscribed component id; null for whole-service subscription","format":"uuid","nullable":true},"component":{"$ref":"#/components/schemas/ServiceComponentDto"},"alertSensitivity":{"type":"string","description":"Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity)","enum":["ALL","INCIDENTS_ONLY","MAJOR_ONLY"]},"subscribedAt":{"type":"string","description":"When the organization subscribed to this service","format":"date-time"}},"description":"An org-level service subscription with current status information"},"SingleValueResponseServiceSubscriptionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceSubscriptionDto"}}},"CreateSecretRequest":{"required":["key","value"],"type":"object","properties":{"key":{"maxLength":255,"minLength":0,"type":"string","description":"Unique secret key within the workspace (max 255 chars)"},"value":{"maxLength":32768,"minLength":0,"type":"string","description":"Secret value, stored encrypted (max 32KB)"}}},"CreateResourceGroupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this group"},"description":{"type":"string","description":"Optional description","nullable":true},"alertPolicyId":{"type":"string","description":"Optional notification policy to apply for this group","format":"uuid","nullable":true},"defaultFrequency":{"maximum":86400,"minimum":30,"type":"integer","description":"Default check frequency in seconds applied to members (30–86400)","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions applied to member monitors","nullable":true,"items":{"type":"string","description":"Default regions applied to member monitors","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs applied to member monitors","nullable":true,"items":{"type":"string","description":"Default alert channel IDs applied to member monitors","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID applied to member monitors","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"maximum":100,"exclusiveMaximum":false,"minimum":0,"exclusiveMinimum":false,"type":"number","description":"Health threshold value: count (0+) or percentage (0–100)","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"Suppress member-level alert notifications when group manages alerting","nullable":true},"confirmationDelaySeconds":{"maximum":600,"minimum":0,"type":"integer","description":"Confirmation delay in seconds before group incident creation (0–600)","format":"int32","nullable":true},"recoveryCooldownMinutes":{"maximum":60,"minimum":0,"type":"integer","description":"Recovery cooldown in minutes after group incident resolves (0–60)","format":"int32","nullable":true}},"description":"Request body for creating a resource group"},"AddResourceGroupMemberRequest":{"required":["memberId","memberType"],"type":"object","properties":{"memberType":{"minLength":1,"pattern":"monitor|service","type":"string","description":"Type of member: 'monitor' or 'service'"},"memberId":{"type":"string","description":"ID of the monitor or service to add","format":"uuid"}},"description":"Request body for adding a member to a resource group"},"SingleValueResponseResourceGroupMemberDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupMemberDto"}}},"CreateNotificationPolicyRequest":{"required":["escalation","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this policy"},"matchRules":{"type":"array","description":"Match rules to evaluate (all must pass; omit or empty for catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is enabled (default true)","default":true},"priority":{"type":"integer","description":"Evaluation priority; higher value = evaluated first (default 0)","format":"int32","default":0}},"description":"Request body for creating a notification policy"},"TestNotificationPolicyRequest":{"type":"object","properties":{"severity":{"type":"string","description":"Incident severity to test against (e.g. DOWN, DEGRADED, MAINTENANCE)","nullable":true},"monitorId":{"type":"string","description":"Monitor UUID to test against (monitoring events)","format":"uuid","nullable":true},"regions":{"type":"array","description":"Affected region identifiers to test against (monitoring events)","nullable":true,"items":{"type":"string","description":"Affected region identifiers to test against (monitoring events)","nullable":true}},"eventType":{"type":"string","description":"Incident event type to test against — short form (e.g. created, resolved, reopened) or full form (e.g. incident.created)","nullable":true},"monitorType":{"type":"string","description":"Monitor check type to test against (e.g. HTTP, DNS, MCP_SERVER)","nullable":true},"serviceId":{"type":"string","description":"Service catalog UUID to test against (status data events)","format":"uuid","nullable":true},"componentName":{"type":"string","description":"Component name to test against (status data events, e.g. \"Actions\")","nullable":true},"resourceGroupIds":{"type":"array","description":"Resource group UUIDs the entity belongs to, for resource_group_id_in rules","nullable":true,"items":{"type":"string","description":"Resource group UUIDs the entity belongs to, for resource_group_id_in rules","format":"uuid","nullable":true}}},"description":"Event context for a dry-run match evaluation against a notification policy"},"SingleValueResponseTestMatchResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestMatchResult"}}},"TestMatchResult":{"type":"object","properties":{"matched":{"type":"boolean","description":"Whether the policy would match the supplied incident context"},"matchedRules":{"type":"array","description":"Rules that passed evaluation","items":{"type":"string","description":"Rules that passed evaluation"}},"unmatchedRules":{"type":"array","description":"Rules that did not pass evaluation","items":{"type":"string","description":"Rules that did not pass evaluation"}}},"description":"Result of a dry-run match evaluation against a notification policy"},"AlertDeliveryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"incidentId":{"type":"string","description":"Incident that triggered this delivery","format":"uuid"},"dispatchId":{"type":"string","description":"Notification dispatch that created this delivery","format":"uuid","nullable":true},"channelId":{"type":"string","description":"Alert channel ID","format":"uuid"},"channel":{"type":"string","description":"Human-readable channel name"},"channelType":{"type":"string","description":"Alert channel type (e.g. slack, email, webhook)"},"status":{"type":"string","description":"Current delivery status","enum":["PENDING","DELIVERED","RETRY_PENDING","FAILED","CANCELLED"]},"eventType":{"type":"string","description":"Incident lifecycle event that triggered this delivery","enum":["INCIDENT_CREATED","INCIDENT_RESOLVED","INCIDENT_REOPENED"]},"stepNumber":{"type":"integer","description":"1-based escalation step this delivery belongs to","format":"int32"},"fireCount":{"type":"integer","description":"Fire sequence within the step: 1 = initial, 2+ = repeat re-fires","format":"int32"},"attemptCount":{"type":"integer","description":"Number of delivery attempts made","format":"int32"},"lastAttemptAt":{"type":"string","description":"When the last attempt was made","format":"date-time","nullable":true},"nextRetryAt":{"type":"string","description":"When the next retry is scheduled (null if not retrying)","format":"date-time","nullable":true},"deliveredAt":{"type":"string","description":"Timestamp when the delivery was confirmed (null if not yet delivered)","format":"date-time","nullable":true},"errorMessage":{"type":"string","description":"Error message from the last failed attempt","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"description":"Delivery record for a single channel within a notification dispatch"},"NotificationDispatchDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"incidentId":{"type":"string","format":"uuid"},"policyId":{"type":"string","format":"uuid"},"policyName":{"type":"string","description":"Human-readable name of the matched policy (null if policy has been deleted)","nullable":true},"status":{"type":"string","description":"Current dispatch state","enum":["PENDING","DISPATCHING","DELIVERED","ESCALATING","ACKNOWLEDGED","COMPLETED"]},"completionReason":{"type":"string","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.","nullable":true,"enum":["EXHAUSTED","RESOLVED","NO_STEPS"]},"currentStep":{"type":"integer","description":"1-based index of the currently active escalation step","format":"int32"},"totalSteps":{"type":"integer","description":"Total number of escalation steps in the policy (null if policy has been deleted)","format":"int32","nullable":true},"acknowledgedAt":{"type":"string","description":"Timestamp when this dispatch was acknowledged (null if not acknowledged)","format":"date-time","nullable":true},"nextEscalationAt":{"type":"string","description":"Timestamp when the next escalation step will fire (null if not scheduled)","format":"date-time","nullable":true},"lastNotifiedAt":{"type":"string","description":"Timestamp of the most recent notification delivery","format":"date-time","nullable":true},"deliveries":{"type":"array","description":"Delivery records for all channels associated with this dispatch","items":{"$ref":"#/components/schemas/AlertDeliveryDto"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"description":"Dispatch state for a single (incident, notification policy) pair, with delivery history"},"SingleValueResponseNotificationDispatchDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationDispatchDto"}}},"CreateMonitorRequest":{"required":["config","managedBy","name","type"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this monitor"},"type":{"type":"string","description":"Monitor protocol type","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"Check frequency in seconds (30–86400, default: 60)","format":"int32"},"enabled":{"type":"boolean","description":"Whether the monitor is active (default: true)","nullable":true},"regions":{"type":"array","description":"Probe regions to run checks from, e.g. us-east, eu-west","nullable":true,"items":{"type":"string","description":"Probe regions to run checks from, e.g. us-east, eu-west","nullable":true}},"managedBy":{"type":"string","description":"Who manages this monitor: DASHBOARD or CLI","enum":["DASHBOARD","CLI"]},"environmentId":{"type":"string","description":"Environment to associate with this monitor","format":"uuid","nullable":true},"assertions":{"type":"array","description":"Assertions to evaluate against each check result","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}},"auth":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]},"incidentPolicy":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"},"alertChannelIds":{"type":"array","description":"Alert channels to notify when this monitor triggers","nullable":true,"items":{"type":"string","description":"Alert channels to notify when this monitor triggers","format":"uuid","nullable":true}},"tags":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"SetMonitorAuthRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"AssertionTestResultDto":{"type":"object","properties":{"assertionType":{"type":"string","enum":["status_code","response_time","body_contains","json_path","header","regex","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"]},"passed":{"type":"boolean"},"severity":{"type":"string","enum":["fail","warn"]},"message":{"type":"string"},"expected":{"type":"string","nullable":true},"actual":{"type":"string","nullable":true}}},"MonitorTestResultDto":{"type":"object","properties":{"passed":{"type":"boolean"},"error":{"type":"string","nullable":true},"statusCode":{"type":"integer","format":"int32","nullable":true},"responseTimeMs":{"type":"integer","format":"int64","nullable":true},"responseHeaders":{"type":"object","additionalProperties":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"nullable":true},"bodyPreview":{"type":"string","nullable":true},"responseSizeBytes":{"type":"integer","format":"int64","nullable":true},"redirectCount":{"type":"integer","format":"int32","nullable":true},"finalUrl":{"type":"string","nullable":true},"assertionResults":{"type":"array","items":{"$ref":"#/components/schemas/AssertionTestResultDto"}},"warnings":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}}}},"SingleValueResponseMonitorTestResultDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorTestResultDto"}}},"TableValueResultTagDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TagDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MonitorTestRequest":{"required":["config","type"],"type":"object","properties":{"type":{"type":"string","description":"Monitor protocol type to test","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"assertions":{"type":"array","description":"Optional assertions to evaluate against the test result","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}}}},"BulkMonitorActionRequest":{"required":["action","monitorIds"],"type":"object","properties":{"monitorIds":{"maxItems":200,"minItems":0,"type":"array","description":"IDs of monitors to act on (max 200)","items":{"type":"string","description":"IDs of monitors to act on (max 200)","format":"uuid"}},"action":{"type":"string","description":"Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG","enum":["PAUSE","RESUME","DELETE","ADD_TAG","REMOVE_TAG"]},"tagIds":{"type":"array","description":"Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)","nullable":true,"items":{"type":"string","description":"Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)","format":"uuid","nullable":true}},"newTags":{"type":"array","description":"New tags to create and attach (only for ADD_TAG)","nullable":true,"items":{"$ref":"#/components/schemas/NewTagRequest"}}},"description":"Request body for performing a bulk action on multiple monitors"},"BulkMonitorActionResult":{"type":"object","properties":{"succeeded":{"type":"array","description":"IDs of monitors on which the action succeeded","items":{"type":"string","description":"IDs of monitors on which the action succeeded","format":"uuid"}},"failed":{"type":"array","description":"Monitors on which the action failed, with the reason for each failure","items":{"$ref":"#/components/schemas/FailureDetail"}}},"description":"Result of a bulk monitor action, including partial-success details"},"FailureDetail":{"type":"object","properties":{"monitorId":{"type":"string","description":"Monitor ID that failed","format":"uuid"},"reason":{"type":"string","description":"Human-readable reason for the failure"}},"description":"Details about a single monitor that failed the bulk action"},"SingleValueResponseBulkMonitorActionResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkMonitorActionResult"}}},"CreateMaintenanceWindowRequest":{"required":["endsAt","startsAt"],"type":"object","properties":{"monitorId":{"type":"string","format":"uuid"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"repeatRule":{"maxLength":100,"minLength":0,"type":"string"},"reason":{"type":"string"},"suppressAlerts":{"type":"boolean"}}},"CreateInviteRequest":{"required":["email","roleOffered"],"type":"object","properties":{"email":{"minLength":1,"type":"string","format":"email"},"roleOffered":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]}}},"InviteDto":{"type":"object","properties":{"inviteId":{"type":"integer","format":"int32"},"email":{"type":"string"},"roleOffered":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]},"expiresAt":{"type":"string","format":"date-time"},"consumedAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseInviteDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/InviteDto"}}},"CreateManualIncidentRequest":{"required":["severity","title"],"type":"object","properties":{"title":{"minLength":1,"type":"string","description":"Short summary of the incident"},"severity":{"type":"string","description":"Incident severity: DOWN, DEGRADED, or MAINTENANCE","enum":["DOWN","DEGRADED","MAINTENANCE"]},"monitorId":{"type":"string","description":"Monitor to associate with this incident","format":"uuid","nullable":true},"body":{"type":"string","description":"Detailed description or context for the incident","nullable":true}}},"IncidentDetailDto":{"type":"object","properties":{"incident":{"$ref":"#/components/schemas/IncidentDto"},"updates":{"type":"array","items":{"$ref":"#/components/schemas/IncidentUpdateDto"}}}},"IncidentUpdateDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"incidentId":{"type":"string","format":"uuid"},"oldStatus":{"type":"string","nullable":true,"enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"newStatus":{"type":"string","nullable":true,"enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"body":{"type":"string","nullable":true},"createdBy":{"type":"string","enum":["SYSTEM","USER"]},"notifySubscribers":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseIncidentDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentDetailDto"}}},"AddIncidentUpdateRequest":{"type":"object","properties":{"body":{"type":"string"},"newStatus":{"type":"string","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"notifySubscribers":{"type":"boolean"}}},"ResolveIncidentRequest":{"type":"object","properties":{"body":{"type":"string","description":"Optional resolution message or post-mortem notes"}}},"CreateEnvironmentRequest":{"required":["name","slug"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Human-readable environment name"},"slug":{"maxLength":100,"minLength":0,"pattern":"^[a-z0-9][a-z0-9_-]*$","type":"string","description":"URL-safe identifier (lowercase alphanumeric, hyphens, underscores)"},"variables":{"type":"object","additionalProperties":{"type":"string","description":"Initial key-value variable pairs for this environment","nullable":true},"description":"Initial key-value variable pairs for this environment","nullable":true},"isDefault":{"type":"boolean","description":"Whether this is the default environment for new monitors"}}},"CreateApiKeyRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"Human-readable name to identify this API key"},"expiresAt":{"type":"string","description":"Optional expiration timestamp in ISO 8601 format","format":"date-time","nullable":true}}},"ApiKeyCreateResponse":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"name":{"type":"string"},"key":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseApiKeyCreateResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}},"ApiKeyDto":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"name":{"type":"string"},"key":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseApiKeyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyDto"}}},"SingleValueResponseAlertDeliveryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AlertDeliveryDto"}}},"CreateAlertChannelRequest":{"required":["config","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this alert channel"},"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}}},"SingleValueResponseTestChannelResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestChannelResult"}}},"TestChannelResult":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}},"TestAlertChannelRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}}},"ComponentUpdateRequest":{"required":["addComponents"],"type":"object","properties":{"addComponents":{"minItems":1,"type":"array","items":{"type":"string"}}}},"UpdateAlertSensitivityRequest":{"required":["alertSensitivity"],"type":"object","properties":{"alertSensitivity":{"minLength":1,"pattern":"ALL|INCIDENTS_ONLY|MAJOR_ONLY","type":"string","description":"Alert sensitivity: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents)"}},"description":"Request body for updating alert sensitivity on a service subscription"},"UpdateApiKeyRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New name for this API key"}}},"TableValueResultWorkspaceDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseMapStringString":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{"type":"string"}}}},"SingleValueResponseListMonitorAssertionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MonitorAssertionDto"}}}},"SchedulableMonitorDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","format":"int32"},"regions":{"type":"array","items":{"type":"string"}},"organizationId":{"type":"integer","format":"int32"}}},"TableValueResultAdapterHealthDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdapterHealthDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseListBillingPlanDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/BillingPlanDto"}}}},"TableValueResultTransactionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TransactionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultSubscriptionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseUpcomingChargeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UpcomingChargeResponse"}}},"UpcomingChargeResponse":{"type":"object","properties":{"action":{"type":"string","enum":["UPGRADE","DOWNGRADE","NOOP"]},"immediateAmount":{"type":"integer","format":"int32"},"nextBillingAmount":{"type":"integer","format":"int32"},"nextBillingDate":{"type":"string","format":"date-time","nullable":true}}},"EntitlementDto":{"type":"object","properties":{"key":{"type":"string","description":"Entitlement key"},"value":{"type":"integer","description":"Effective limit value (overrides applied)","format":"int64"},"defaultValue":{"type":"integer","description":"Plan-tier default value before overrides","format":"int64"},"overridden":{"type":"boolean","description":"Whether this entitlement has an org-level override"}},"description":"A single resolved entitlement for the organization"},"EntitlementResponse":{"type":"object","properties":{"tier":{"type":"string","description":"Resolved billing plan tier","enum":["FREE","STARTER","PRO","TEAM","BUSINESS","ENTERPRISE"]},"entitlements":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/EntitlementDto"},"description":"All entitlements keyed by entitlement key"},"usage":{"type":"object","additionalProperties":{"type":"integer","description":"Current usage counters keyed by entitlement key (only for countable resources)","format":"int64"},"description":"Current usage counters keyed by entitlement key (only for countable resources)"},"trialActive":{"type":"boolean","description":"Whether the org is currently on a trial"},"trialExpiresAt":{"type":"string","description":"Trial expiry date (null if not trialing)","format":"date-time","nullable":true},"subscriptionStatus":{"type":"string","description":"Current subscription status (null if no subscription)","nullable":true}},"description":"Full entitlement state for an organization: resolved limits, usage, and trial info"},"SingleValueResponseEntitlementResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EntitlementResponse"}}},"PaginationParams":{"required":["sortBy","sortOrder"],"type":"object","properties":{"sortBy":{"type":"string"},"sortOrder":{"type":"string","enum":["ASC","DESC"]},"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"maximum":200,"minimum":1,"type":"integer","format":"int32"}}},"IdValuePair":{"type":"object","properties":{"id":{"type":"integer","format":"int32"},"value":{"type":"string"}}},"TableValueResultIdValuePair":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IdValuePair"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MyOrgItemDto":{"type":"object","properties":{"orgId":{"type":"integer","format":"int32"},"orgName":{"type":"string"},"orgRole":{"type":"string","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}}},"TableValueResultMyOrgItemDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MyOrgItemDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64","nullable":true}}},"Pageable":{"type":"object","properties":{"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"minimum":1,"type":"integer","format":"int32"},"sort":{"type":"array","items":{"type":"string"}}}},"TableValueResultUserDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UserDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"AdminStatsDto":{"type":"object","properties":{"userCount":{"type":"integer","format":"int64"},"orgCount":{"type":"integer","format":"int64"},"memberCount":{"type":"integer","format":"int64"}}},"SingleValueResponseAdminStatsDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminStatsDto"}}},"TableValueResultOrganizationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultMemberDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MemberDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultWebhookEndpointDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpointDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultWebhookDeliveryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"WebhookDeliveryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"endpointId":{"type":"string","format":"uuid"},"eventId":{"type":"string"},"eventType":{"type":"string"},"status":{"type":"string"},"attemptCount":{"type":"integer","format":"int32"},"maxAttempts":{"type":"integer","format":"int32"},"responseStatus":{"type":"integer","format":"int32","nullable":true},"responseLatencyMs":{"type":"integer","format":"int32","nullable":true},"errorMessage":{"type":"string","nullable":true},"deliveredAt":{"type":"string","format":"date-time","nullable":true},"failedAt":{"type":"string","format":"date-time","nullable":true},"nextRetryAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseWebhookSigningSecretDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSigningSecretDto"}}},"WebhookSigningSecretDto":{"type":"object","properties":{"configured":{"type":"boolean"},"maskedSecret":{"type":"string","nullable":true}}},"WebhookEventCatalogEntry":{"type":"object","properties":{"type":{"type":"string","description":"Dot-notation event type identifier, e.g. \"monitor.created\""},"surface":{"type":"string","description":"Product surface this event belongs to, e.g. \"monitoring\" or \"status_data\""},"description":{"type":"string","description":"Human-readable description of when this event fires"}}},"WebhookEventCatalogResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventCatalogEntry"}}}},"CursorPageServiceCatalogDto":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"$ref":"#/components/schemas/ServiceCatalogDto"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"ServiceCatalogDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"developerContext":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"overallStatus":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"componentCount":{"type":"integer","format":"int64"},"activeIncidentCount":{"type":"integer","format":"int64"},"dataCompleteness":{"type":"string"}},"description":"Items on this page"},"MaintenanceComponentRef":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"status":{"type":"string"}}},"MaintenanceUpdateDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"body":{"type":"string","nullable":true},"displayAt":{"type":"string","format":"date-time","nullable":true}},"description":"A status update within a scheduled maintenance lifecycle"},"ScheduledMaintenanceDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"shortlink":{"type":"string","nullable":true},"scheduledFor":{"type":"string","format":"date-time","nullable":true},"scheduledUntil":{"type":"string","format":"date-time","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true},"affectedComponents":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceComponentRef"}},"updates":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceUpdateDto"}}},"description":"A scheduled maintenance window from a vendor status page"},"ServiceDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"developerContext":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"currentStatus":{"$ref":"#/components/schemas/ServiceStatusDto"},"recentIncidents":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentDto"}},"components":{"type":"array","items":{"$ref":"#/components/schemas/ServiceComponentDto"}},"uptime":{"$ref":"#/components/schemas/ComponentUptimeSummaryDto"},"activeMaintenances":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledMaintenanceDto"}},"dataCompleteness":{"type":"string"}}},"ServiceIncidentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"serviceSlug":{"type":"string","nullable":true},"serviceName":{"type":"string","nullable":true},"externalId":{"type":"string","nullable":true},"title":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true},"shortlink":{"type":"string","nullable":true},"detectedAt":{"type":"string","format":"date-time","nullable":true},"vendorCreatedAt":{"type":"string","format":"date-time","nullable":true}}},"ServiceStatusDto":{"type":"object","properties":{"overallStatus":{"type":"string"},"lastPolledAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseServiceDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceDetailDto"}}},"ServiceUptimeResponse":{"type":"object","properties":{"overallUptimePct":{"type":"number","description":"Overall uptime percentage across the entire period; null when no polling data exists","format":"double","nullable":true,"example":99.95},"period":{"type":"string","description":"Requested period","example":"7d"},"granularity":{"type":"string","description":"Requested granularity","example":"hourly"},"buckets":{"type":"array","description":"Per-bucket breakdown ordered by time ascending","items":{"$ref":"#/components/schemas/UptimeBucketDto"}},"source":{"type":"string","description":"Data source: vendor_reported, incident_derived, or poll_derived","nullable":true,"example":"vendor_reported"}},"description":"Uptime response with per-bucket breakdown and overall percentage for the period"},"SingleValueResponseServiceUptimeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceUptimeResponse"}}},"UptimeBucketDto":{"type":"object","properties":{"timestamp":{"type":"string","description":"Start of the bucket interval (ISO 8601)","format":"date-time","example":"2024-01-01T00:00:00Z"},"uptimePct":{"type":"number","description":"Uptime percentage for this bucket; null when no polls occurred","format":"double","nullable":true,"example":100.0},"totalPolls":{"type":"integer","description":"Total number of polls recorded in this bucket","format":"int64","example":12}},"description":"Uptime statistics for a single time bucket"},"TableValueResultScheduledMaintenanceDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledMaintenanceDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultServiceIncidentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"ServiceIncidentDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"detectedAt":{"type":"string","format":"date-time","nullable":true},"shortlink":{"type":"string","nullable":true},"affectedComponents":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"updates":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentUpdateDto"}}}},"ServiceIncidentUpdateDto":{"type":"object","properties":{"status":{"type":"string"},"body":{"type":"string","nullable":true},"displayAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseServiceIncidentDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceIncidentDetailDto"}}},"TableValueResultServiceComponentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceComponentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"ComponentUptimeDayDto":{"type":"object","properties":{"date":{"type":"string","format":"date-time"},"partialOutageSeconds":{"type":"integer","format":"int32"},"majorOutageSeconds":{"type":"integer","format":"int32"},"uptimePercentage":{"type":"number","format":"double"},"eventsJson":{"type":"string","description":"Incident event references for this day as raw JSON","nullable":true},"source":{"type":"string"}},"description":"Daily uptime data for a component"},"TableValueResultComponentUptimeDayDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ComponentUptimeDayDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"GlobalStatusSummaryDto":{"type":"object","properties":{"totalServices":{"type":"integer","format":"int32"},"operationalCount":{"type":"integer","format":"int32"},"degradedCount":{"type":"integer","format":"int32"},"partialOutageCount":{"type":"integer","format":"int32"},"majorOutageCount":{"type":"integer","format":"int32"},"maintenanceCount":{"type":"integer","format":"int32"},"activeIncidentCount":{"type":"integer","format":"int64"},"servicesWithIssues":{"type":"array","items":{"$ref":"#/components/schemas/ServiceCatalogDto"}}}},"SingleValueResponseGlobalStatusSummaryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/GlobalStatusSummaryDto"}}},"TableValueResultServiceSubscriptionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceSubscriptionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultSecretDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SecretDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultResourceGroupDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResourceGroupDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseResourceGroupHealthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupHealthDto"}}},"NotificationDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"type":{"type":"string"},"title":{"type":"string"},"body":{"type":"string","nullable":true},"resourceType":{"type":"string","nullable":true},"resourceId":{"type":"string","nullable":true},"read":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"TableValueResultNotificationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseLong":{"type":"object","properties":{"data":{"type":"integer","format":"int64"}}},"TableValueResultNotificationPolicyDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationPolicyDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultNotificationDispatchDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationDispatchDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultMonitorDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MonitorDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MonitorVersionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"snapshot":{"$ref":"#/components/schemas/MonitorDto"},"changedById":{"type":"integer","format":"int32","nullable":true},"changedVia":{"type":"string","enum":["API","DASHBOARD","CLI","TERRAFORM"]},"changeSummary":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"TableValueResultMonitorVersionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MonitorVersionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseMonitorVersionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorVersionDto"}}},"UptimeDto":{"type":"object","properties":{"uptimePercentage":{"type":"number","description":"Uptime percentage over the requested window; null when no data","format":"double","nullable":true,"example":99.95},"totalChecks":{"type":"integer","description":"Total number of checks executed","format":"int64","example":1440},"passedChecks":{"type":"integer","description":"Number of checks that passed","format":"int64","example":1439},"avgLatencyMs":{"type":"number","description":"Weighted average latency in milliseconds; null when no data","format":"double","nullable":true,"example":142.5},"p95LatencyMs":{"type":"number","description":"95th-percentile latency in milliseconds (upper bound across regions); null when no data","format":"double","nullable":true,"example":312.0}},"description":"Uptime statistics aggregated from continuous aggregates"},"SingleValueResponseUptimeDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UptimeDto"}}},"CursorPage":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"type":"object","description":"Items on this page"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"AssertionResultDto":{"type":"object","properties":{"type":{"type":"string","description":"Assertion type","example":"status_code"},"passed":{"type":"boolean","description":"Whether the assertion passed"},"severity":{"type":"string","description":"Assertion severity","enum":["fail","warn"]},"message":{"type":"string","description":"Human-readable result message","nullable":true},"expected":{"type":"string","description":"Expected value","nullable":true,"example":"200"},"actual":{"type":"string","description":"Actual value observed","nullable":true,"example":"503"}},"description":"Result of evaluating a single assertion against a check result"},"CheckResultDetailsDto":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code of the response","format":"int32","nullable":true,"example":200},"responseHeaders":{"type":"object","additionalProperties":{"type":"array","description":"HTTP response headers","nullable":true,"items":{"type":"string","description":"HTTP response headers","nullable":true}},"description":"HTTP response headers","nullable":true},"responseBodySnapshot":{"type":"string","description":"Raw response body snapshot (may be HTML, XML, JSON, or plain text)","nullable":true},"assertionResults":{"type":"array","description":"Individual assertion evaluation results","nullable":true,"items":{"$ref":"#/components/schemas/AssertionResultDto"}},"tlsInfo":{"$ref":"#/components/schemas/TlsInfoDto"},"redirectCount":{"type":"integer","description":"Number of HTTP redirects followed","format":"int32","nullable":true,"example":2},"redirectTarget":{"type":"string","description":"Final URL after redirects","nullable":true},"responseSizeBytes":{"type":"integer","description":"Response body size in bytes","format":"int32","nullable":true,"example":4096},"checkDetails":{"oneOf":[{"$ref":"#/components/schemas/Dns"},{"$ref":"#/components/schemas/Http"},{"$ref":"#/components/schemas/Icmp"},{"$ref":"#/components/schemas/McpServer"},{"$ref":"#/components/schemas/Tcp"}]}},"description":"Type-specific details captured during a check execution"},"CheckResultDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier of the check result","format":"uuid"},"timestamp":{"type":"string","description":"Timestamp when the check was executed (ISO 8601)","format":"date-time"},"region":{"type":"string","description":"Region where the check was executed","example":"us-east"},"responseTimeMs":{"type":"integer","description":"Response time in milliseconds","format":"int32","nullable":true,"example":123},"passed":{"type":"boolean","description":"Whether the check passed","example":true},"failureReason":{"type":"string","description":"Reason for failure when passed=false","nullable":true},"severityHint":{"type":"string","description":"Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing","nullable":true},"details":{"$ref":"#/components/schemas/CheckResultDetailsDto"}},"description":"A single check result from a monitor run"},"CheckTypeDetailsDto":{"required":["check_type"],"type":"object","properties":{"check_type":{"type":"string"}},"description":"Check-type-specific details — polymorphic by check_type discriminator","discriminator":{"propertyName":"check_type"}},"CursorPageCheckResultDto":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"$ref":"#/components/schemas/CheckResultDto"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"Dns":{"type":"object","description":"DNS check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"hostname":{"type":"string","description":"Target hostname","nullable":true},"requestedTypes":{"type":"array","description":"Requested DNS record types","nullable":true,"items":{"type":"string","description":"Requested DNS record types","nullable":true}},"usedResolver":{"type":"string","description":"Resolver used for lookup","nullable":true},"records":{"type":"object","additionalProperties":{"type":"array","description":"Resolved DNS records keyed by record type","nullable":true,"items":{"type":"object","additionalProperties":{"type":"object","description":"Resolved DNS records keyed by record type","nullable":true},"description":"Resolved DNS records keyed by record type","nullable":true}},"description":"Resolved DNS records keyed by record type","nullable":true},"attempts":{"type":"array","description":"DNS resolution attempts","nullable":true,"items":{"type":"object","additionalProperties":{"type":"object","description":"DNS resolution attempts","nullable":true},"description":"DNS resolution attempts","nullable":true}},"failureKind":{"type":"string","description":"Kind of DNS failure, if any","nullable":true}}}]},"Http":{"type":"object","description":"HTTP check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"timing":{"type":"object","additionalProperties":{"type":"object","description":"Request phase timing breakdown","nullable":true},"description":"Request phase timing breakdown","nullable":true},"bodyTruncated":{"type":"boolean","description":"Whether the response body was truncated before storage","nullable":true}}}]},"Icmp":{"type":"object","description":"ICMP (ping) check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"host":{"type":"string","description":"Target host","example":"1.1.1.1"},"packetsSent":{"type":"integer","description":"Number of ICMP packets sent","format":"int32","nullable":true},"packetsReceived":{"type":"integer","description":"Number of ICMP packets received","format":"int32","nullable":true},"packetLoss":{"type":"number","description":"Packet loss percentage","format":"double","nullable":true,"example":0.0},"avgRttMs":{"type":"number","description":"Average round-trip time in ms","format":"double","nullable":true},"minRttMs":{"type":"number","description":"Minimum round-trip time in ms","format":"double","nullable":true},"maxRttMs":{"type":"number","description":"Maximum round-trip time in ms","format":"double","nullable":true},"jitterMs":{"type":"number","description":"Jitter in ms","format":"double","nullable":true}}}]},"McpServer":{"type":"object","description":"MCP server check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"url":{"type":"string","description":"MCP server URL","nullable":true},"protocolVersion":{"type":"string","description":"MCP protocol version","nullable":true},"serverInfo":{"type":"object","additionalProperties":{"type":"object","description":"MCP server info (name, version, etc.)","nullable":true},"description":"MCP server info (name, version, etc.)","nullable":true},"toolCount":{"type":"integer","description":"Number of tools exposed","format":"int32","nullable":true},"resourceCount":{"type":"integer","description":"Number of resources exposed","format":"int32","nullable":true},"promptCount":{"type":"integer","description":"Number of prompts exposed","format":"int32","nullable":true}}}]},"Tcp":{"type":"object","description":"TCP check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"host":{"type":"string","description":"Target host","example":"db.example.com"},"port":{"type":"integer","description":"Target port","format":"int32","example":5432},"connected":{"type":"boolean","description":"Whether a TCP connection was established"}}}]},"TlsInfoDto":{"type":"object","properties":{"subjectCn":{"type":"string","description":"Certificate subject common name","nullable":true,"example":"*.example.com"},"subjectSan":{"type":"array","description":"Subject Alternative Names","nullable":true,"items":{"type":"string","description":"Subject Alternative Names","nullable":true}},"issuerCn":{"type":"string","description":"Issuer common name","nullable":true,"example":"R3"},"issuerOrg":{"type":"string","description":"Issuer organisation","nullable":true,"example":"Let's Encrypt"},"notBefore":{"type":"string","description":"Certificate validity start (ISO 8601 UTC)","nullable":true},"notAfter":{"type":"string","description":"Certificate validity end (ISO 8601 UTC)","nullable":true},"serialNumber":{"type":"string","description":"Certificate serial number","nullable":true},"tlsVersion":{"type":"string","description":"TLS protocol version","nullable":true,"example":"TLSv1.3"},"cipherSuite":{"type":"string","description":"Negotiated cipher suite","nullable":true},"chainValid":{"type":"boolean","description":"Whether the chain validated against the OS trust store","nullable":true}},"description":"TLS/SSL certificate details for HTTPS targets"},"ChartBucketDto":{"type":"object","properties":{"bucket":{"type":"string","description":"Start of the time bucket (ISO 8601)","format":"date-time","example":"2026-03-12T10:00:00Z"},"uptimePercent":{"type":"number","description":"Uptime percentage for this bucket; null when no data","format":"double","nullable":true,"example":100.0},"avgLatencyMs":{"type":"number","description":"Weighted average latency in milliseconds for this bucket","format":"double","nullable":true,"example":120.3},"p95LatencyMs":{"type":"number","description":"95th percentile latency in milliseconds (max across regions)","format":"double","nullable":true,"example":250.0},"p99LatencyMs":{"type":"number","description":"99th percentile latency in milliseconds (max across regions)","format":"double","nullable":true,"example":480.0}},"description":"Aggregated metrics for a time bucket"},"RegionStatusDto":{"type":"object","properties":{"region":{"type":"string","description":"Region identifier","example":"us-east"},"passed":{"type":"boolean","description":"Whether the last check in this region passed","example":true},"responseTimeMs":{"type":"integer","description":"Response time in milliseconds for the last check","format":"int32","nullable":true,"example":95},"timestamp":{"type":"string","description":"Timestamp of the last check in this region (ISO 8601)","format":"date-time"},"severityHint":{"type":"string","description":"Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing","nullable":true}},"description":"Latest check result for a single region"},"ResultSummaryDto":{"type":"object","properties":{"currentStatus":{"type":"string","description":"Derived current status across all regions","enum":["up","degraded","down","unknown"]},"latestPerRegion":{"type":"array","description":"Latest check result per region","items":{"$ref":"#/components/schemas/RegionStatusDto"}},"chartData":{"type":"array","description":"Time-bucketed chart data for the requested window","items":{"$ref":"#/components/schemas/ChartBucketDto"}},"uptime24h":{"type":"number","description":"Uptime percentage over the last 24 hours; null when no data","format":"double","nullable":true,"example":99.95},"uptimeWindow":{"type":"number","description":"Uptime percentage for the selected chart window; null when no data","format":"double","nullable":true,"example":99.8}},"description":"Dashboard summary: current status, per-region latest results, and chart data"},"SingleValueResponseResultSummaryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResultSummaryDto"}}},"TableValueResultMaintenanceWindowDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceWindowDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultInviteDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InviteDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"IntegrationCatalogResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationDto"}}}},"IntegrationConfigSchemaDto":{"type":"object","properties":{"connectionFields":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationFieldDto"}},"channelFields":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationFieldDto"}}}},"IntegrationDto":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"logoUrl":{"type":"string"},"authType":{"type":"string"},"tierAvailability":{"type":"string","enum":["FREE","STARTER","PRO","TEAM","BUSINESS","ENTERPRISE"]},"lifecycle":{"type":"string"},"setupGuideUrl":{"type":"string"},"configSchema":{"$ref":"#/components/schemas/IntegrationConfigSchemaDto"}}},"IntegrationFieldDto":{"required":["key","label","required","sensitive","type"],"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"sensitive":{"type":"boolean"},"placeholder":{"type":"string","nullable":true},"helpText":{"type":"string","nullable":true},"options":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"default":{"type":"string","nullable":true}}},"IncidentFilterParams":{"type":"object","properties":{"status":{"type":"string","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"severity":{"type":"string","enum":["DOWN","DEGRADED","MAINTENANCE"]},"source":{"type":"string","enum":["AUTOMATIC","MANUAL","MONITORS","STATUS_DATA","RESOURCE_GROUP"]},"monitorId":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"resourceGroupId":{"type":"string","format":"uuid"},"tagId":{"type":"string","format":"uuid","nullable":true},"environmentId":{"type":"string","format":"uuid","nullable":true},"startedFrom":{"type":"string","format":"date-time","nullable":true},"startedTo":{"type":"string","format":"date-time","nullable":true},"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"maximum":200,"minimum":1,"type":"integer","format":"int32"}}},"TableValueResultEnvironmentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EnvironmentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"DashboardOverviewDto":{"type":"object","properties":{"monitors":{"$ref":"#/components/schemas/MonitorsSummaryDto"},"incidents":{"$ref":"#/components/schemas/IncidentsSummaryDto"}}},"IncidentsSummaryDto":{"type":"object","properties":{"active":{"type":"integer","format":"int64"},"resolvedToday":{"type":"integer","format":"int64"},"mttr30d":{"type":"number","format":"double","nullable":true}}},"MonitorsSummaryDto":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"up":{"type":"integer","format":"int64"},"down":{"type":"integer","format":"int64"},"degraded":{"type":"integer","format":"int64"},"paused":{"type":"integer","format":"int64"},"avgUptime24h":{"type":"number","format":"double","nullable":true},"avgUptime30d":{"type":"number","format":"double","nullable":true}}},"SingleValueResponseDashboardOverviewDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DashboardOverviewDto"}}},"CategoryDto":{"type":"object","properties":{"category":{"type":"string"},"serviceCount":{"type":"integer","format":"int64"}}},"TableValueResultCategoryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CategoryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"AuditEventDto":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"actorId":{"type":"integer","format":"int32","nullable":true},"actorEmail":{"type":"string","nullable":true},"action":{"type":"string"},"resourceType":{"type":"string","nullable":true},"resourceId":{"type":"string","nullable":true},"resourceName":{"type":"string","nullable":true},"metadata":{"type":"object","additionalProperties":{"type":"object","nullable":true},"nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"PageResultAuditEventDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEventDto"}},"page":{"type":"integer","format":"int32"},"size":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"totalPages":{"type":"integer","format":"int32"},"hasNext":{"type":"boolean"}}},"TableValueResultApiKeyDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"DeliveryAttemptDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"deliveryId":{"type":"string","format":"uuid"},"attemptNumber":{"type":"integer","description":"1-based attempt number","format":"int32"},"status":{"type":"string","description":"Outcome: SUCCESS, FAILED, TIMEOUT, ERROR"},"responseStatusCode":{"type":"integer","description":"HTTP response status code from the external service","format":"int32","nullable":true},"requestPayload":{"type":"string","description":"JSON payload sent to the external service","nullable":true},"responseBody":{"type":"string","description":"Response body from the external service (truncated)","nullable":true},"errorMessage":{"type":"string","description":"Error message if the attempt failed","nullable":true},"responseTimeMs":{"type":"integer","description":"Round-trip time in milliseconds","format":"int32","nullable":true},"externalId":{"type":"string","description":"External identifier (e.g. PagerDuty dedup_key, SES MessageId, webhook delivery UUID)","nullable":true},"requestHeaders":{"type":"object","additionalProperties":{"type":"string","description":"HTTP request headers sent to the external service","nullable":true},"description":"HTTP request headers sent to the external service","nullable":true},"attemptedAt":{"type":"string","format":"date-time"}},"description":"Single delivery attempt with request/response audit data"},"TableValueResultDeliveryAttemptDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAttemptDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultAlertChannelDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AlertChannelDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultAlertDeliveryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AlertDeliveryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"RemoveMonitorTagsRequest":{"required":["tagIds"],"type":"object","properties":{"tagIds":{"minItems":1,"type":"array","description":"IDs of the tags to detach from the monitor","items":{"type":"string","description":"IDs of the tags to detach from the monitor","format":"uuid"}}},"description":"Request body for removing tags from a monitor"},"DeleteChannelResult":{"type":"object","properties":{"affectedPolicies":{"type":"integer","description":"Number of notification policies whose escalation steps were modified","format":"int32"},"disabledPolicies":{"type":"integer","description":"Number of notification policies disabled because they had no remaining channels","format":"int32"}},"description":"Summary of policies affected by channel deletion"}},"securitySchemes":{"BearerAuth":{"type":"http","description":"API key (dh_live_...) or Auth0 JWT token","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file +{ + "openapi": "3.0.1", + "info": { + "title": "DevHelm API", + "description": "DevHelm platform and public API", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Heartbeat", + "description": "Public ping endpoint for heartbeat monitors" + }, + { + "name": "Invites", + "description": "Organization invite management" + }, + { + "name": "Onboarding", + "description": "User onboarding flow" + }, + { + "name": "Members", + "description": "Organization member management" + }, + { + "name": "Me", + "description": "Current user profile and organizations" + }, + { + "name": "Incidents", + "description": "Incident management and lifecycle" + }, + { + "name": "Maintenance Windows", + "description": "Schedule alert-suppression windows for monitors" + }, + { + "name": "Organizations", + "description": "Organization management" + }, + { + "name": "Integrations", + "description": "Static catalog of supported alert channel integrations" + }, + { + "name": "Incident Policies", + "description": "Manage trigger, confirmation, and recovery rules for monitors" + }, + { + "name": "Entitlements", + "description": "Plan entitlements and usage limits" + }, + { + "name": "Vault", + "description": "Organization vault management (admin-only)" + }, + { + "name": "Secrets", + "description": "Organization environment secret management" + }, + { + "name": "Transactions", + "description": "Subscription transaction history" + }, + { + "name": "Monitors", + "description": "Monitor CRUD and lifecycle management" + }, + { + "name": "Webhooks", + "description": "Webhook endpoint management, event catalog, and delivery history" + }, + { + "name": "Events", + "description": "Real-time event stream" + }, + { + "name": "Workspaces", + "description": "Workspace management within an organization" + }, + { + "name": "Notifications", + "description": "In-app notification center" + }, + { + "name": "Alert Channels", + "description": "Alert channel CRUD and connectivity testing" + }, + { + "name": "Subscriptions", + "description": "Organization subscription management" + }, + { + "name": "Service Subscriptions", + "description": "Manage which services an organization tracks" + }, + { + "name": "Tags", + "description": "Org-scoped tag management for monitors" + }, + { + "name": "Status Data", + "description": "Public service status catalog, components, uptime, and incident history" + }, + { + "name": "Check Results", + "description": "Query raw check results, uptime statistics, and summary data" + }, + { + "name": "API Keys", + "description": "Organization API key management" + }, + { + "name": "Dashboard", + "description": "Overview dashboard aggregates" + }, + { + "name": "Auth", + "description": "User registration" + }, + { + "name": "Monitor Auth", + "description": "Manage authentication configuration for a monitor" + }, + { + "name": "Audit Log", + "description": "Organization audit trail" + }, + { + "name": "Monitor Alert Channels", + "description": "Manage alert channel mappings for a monitor" + }, + { + "name": "Alert Deliveries", + "description": "Delivery audit trail: inspect per-attempt details for alert deliveries" + }, + { + "name": "API Auth", + "description": "Identity and quota info for API key authentication" + }, + { + "name": "Resource Groups", + "description": "Resource group CRUD and member management" + }, + { + "name": "Notification Policies", + "description": "Org-level notification routing policies with JSONB match rules" + }, + { + "name": "Notification Dispatches", + "description": "Dispatch debugging API: inspect which policies matched an incident and track delivery status" + }, + { + "name": "Environments", + "description": "Variable namespace management for monitors" + }, + { + "name": "Monitor Assertions", + "description": "Manage assertions for a monitor" + }, + { + "name": "Deploy Lock", + "description": "Mutex for CLI deploy operations" + }, + { + "name": "Billing", + "description": "Billing plans and pricing" + } + ], + "paths": { + "/platform/orgs/{orgId}/subscriptions/{subscriptionId}": { + "put": { + "tags": [ + "Subscriptions" + ], + "summary": "Update subscription", + "operationId": "updateSubscription", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "subscriptionId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSubscriptionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseSubscriptionDto" + } + } + } + } + } + } + }, + "/platform/onboarding/orgs/{orgId}/details": { + "put": { + "tags": [ + "Onboarding" + ], + "summary": "Update organization details", + "operationId": "updateOrgDetails", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgDetailsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + } + } + } + }, + "/platform/onboarding/advance": { + "put": { + "tags": [ + "Onboarding" + ], + "summary": "Advance onboarding stage forward", + "operationId": "advanceStage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOnboardingStageRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + } + }, + "/platform/me": { + "get": { + "tags": [ + "Me" + ], + "summary": "Get current user", + "operationId": "me", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Me" + ], + "summary": "Update current user profile", + "operationId": "updateProfile", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProfileRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + } + }, + "/platform/me/notification-preferences": { + "get": { + "tags": [ + "Me" + ], + "summary": "Get current user's notification preferences", + "operationId": "getNotificationPreferences", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPreferencesDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Me" + ], + "summary": "Update current user's notification preferences", + "operationId": "updateNotificationPreferences", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNotificationPreferencesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPreferencesDto" + } + } + } + } + } + } + }, + "/platform/admin/workspaces/{workspaceId}": { + "get": { + "tags": [ + "admin-workspace-controller" + ], + "operationId": "getWorkspace", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "admin-workspace-controller" + ], + "operationId": "updateWorkspace", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "admin-workspace-controller" + ], + "operationId": "deleteWorkspace", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/platform/admin/users/{userId}": { + "put": { + "tags": [ + "admin-controller" + ], + "operationId": "updateUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + } + }, + "/platform/admin/orgs/{orgId}": { + "put": { + "tags": [ + "admin-controller" + ], + "operationId": "updateOrganization", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgDetailsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + } + } + } + }, + "/platform/admin/orgs/{orgId}/members/{userId}/role": { + "put": { + "tags": [ + "admin-member-controller" + ], + "operationId": "updateMemberRole", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/workspaces/{workspaceId}": { + "get": { + "tags": [ + "Workspaces" + ], + "summary": "Get workspace by ID", + "operationId": "get", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Workspaces" + ], + "summary": "Update workspace", + "operationId": "update", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Workspaces" + ], + "summary": "Delete workspace", + "operationId": "delete", + "parameters": [ + { + "name": "workspaceId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/webhooks/{id}": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Get a single webhook endpoint", + "operationId": "get_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Webhooks" + ], + "summary": "Update a webhook endpoint", + "operationId": "update_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Webhooks" + ], + "summary": "Delete a webhook endpoint", + "operationId": "delete_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/tags/{id}": { + "put": { + "tags": [ + "Tags" + ], + "summary": "Update a tag's name and/or color", + "operationId": "update_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateTagRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTagDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Tags" + ], + "summary": "Delete a tag (cascades to all monitor associations)", + "operationId": "delete_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/secrets/{key}": { + "put": { + "tags": [ + "Secrets" + ], + "summary": "Update secret", + "operationId": "update_3", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSecretRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseSecretDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Secrets" + ], + "summary": "Delete secret", + "operationId": "delete_3", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/resource-groups/{id}": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "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.", + "operationId": "get_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includeMetrics", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Resource Groups" + ], + "summary": "Update a resource group's name, description, alert policy, inherited settings, and health threshold", + "operationId": "update_4", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateResourceGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Resource Groups" + ], + "summary": "Delete a resource group (cascades to member rows)", + "operationId": "delete_4", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/org": { + "get": { + "tags": [ + "Organizations" + ], + "summary": "Get the current organization", + "operationId": "get_3", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Organizations" + ], + "summary": "Update the current organization", + "operationId": "update_5", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrgDetailsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "put": { + "tags": [ + "Notifications" + ], + "summary": "Mark a notification as read", + "operationId": "markRead", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/notifications/read-all": { + "put": { + "tags": [ + "Notifications" + ], + "summary": "Mark all notifications as read", + "operationId": "markAllRead", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/notification-policies/{id}": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "Get a notification policy by ID", + "operationId": "getById", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Notification Policies" + ], + "summary": "Update a notification policy", + "operationId": "update_6", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Notification Policies" + ], + "summary": "Delete a notification policy", + "operationId": "delete_5", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/monitors/{monitorId}/policy": { + "get": { + "tags": [ + "Incident Policies" + ], + "summary": "Get incident policy for a monitor", + "description": "Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.", + "operationId": "get_4", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "description": "Monitor UUID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Policy found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IncidentPolicyDto" + } + } + } + }, + "404": { + "description": "Monitor or policy not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Incident Policies" + ], + "summary": "Update incident policy for a monitor", + "description": "Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.", + "operationId": "update_7", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "description": "Monitor UUID", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Policy updated", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IncidentPolicyDto" + } + } + } + }, + "400": { + "description": "Validation error in JSONB shape", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + }, + "404": { + "description": "Monitor or policy not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/auth": { + "put": { + "tags": [ + "Monitor Auth" + ], + "summary": "Update authentication config for a monitor", + "operationId": "update_8", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMonitorAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitor Auth" + ], + "summary": "Set authentication config for a monitor", + "operationId": "set", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetMonitorAuthRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitor Auth" + ], + "summary": "Remove authentication config from a monitor", + "operationId": "remove", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/monitors/{monitorId}/assertions/{assertionId}": { + "put": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Update an assertion on a monitor", + "operationId": "update_9", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "assertionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAssertionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Remove an assertion from a monitor", + "operationId": "remove_1", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "assertionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/monitors/{monitorId}/alert-channels": { + "put": { + "tags": [ + "Monitor Alert Channels" + ], + "summary": "Replace the linked alert channel set for a monitor", + "operationId": "setChannels", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetAlertChannelsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseListUUID" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get a single monitor by id", + "operationId": "get_5", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Monitors" + ], + "summary": "Update a monitor", + "operationId": "update_10", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitors" + ], + "summary": "Soft-delete a monitor", + "operationId": "delete_6", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/members/{userId}/status": { + "put": { + "tags": [ + "Members" + ], + "summary": "Change member status", + "operationId": "changeStatus", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeStatusRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/members/{userId}/role": { + "put": { + "tags": [ + "Members" + ], + "summary": "Change member role", + "operationId": "changeRole", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangeRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/maintenance-windows/{id}": { + "get": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Get a single maintenance window by ID", + "operationId": "getById_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Update a maintenance window", + "operationId": "update_11", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateMaintenanceWindowRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Maintenance Windows" + ], + "summary": "Delete a maintenance window", + "operationId": "delete_7", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/environments/{slug}": { + "get": { + "tags": [ + "Environments" + ], + "summary": "Get environment by slug", + "operationId": "get_6", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + } + } + }, + "put": { + "tags": [ + "Environments" + ], + "summary": "Update environment", + "operationId": "update_12", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Environments" + ], + "summary": "Delete environment", + "operationId": "delete_8", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/alert-channels/{id}": { + "put": { + "tags": [ + "Alert Channels" + ], + "summary": "Update an alert channel's name and re-encrypt config", + "operationId": "update_13", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAlertChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAlertChannelDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Alert Channels" + ], + "summary": "Soft-delete an alert channel and return affected policy summary", + "operationId": "delete_9", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DeleteChannelResult" + } + } + } + } + } + } + }, + "/v1/webhooks/paddle": { + "post": { + "tags": [ + "paddle-webhook-controller" + ], + "operationId": "handleWebhook", + "parameters": [ + { + "name": "paddle-signature", + "in": "header", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/internal/workspaces": { + "post": { + "tags": [ + "workspaces-controller" + ], + "operationId": "create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkspaceCreateParams" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + } + }, + "/v1/internal/service-incidents": { + "post": { + "tags": [ + "service-incident-internal-controller" + ], + "operationId": "createOrResolve", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceIncidentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIncidentDto" + } + } + } + } + } + } + }, + "/v1/internal/resource-groups/services/{serviceId}/re-evaluate-health": { + "post": { + "tags": [ + "resource-groups-internal-controller" + ], + "operationId": "reEvaluateGroupHealthForService", + "parameters": [ + { + "name": "serviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInteger" + } + } + } + } + } + } + }, + "/v1/internal/resource-groups/monitors/{monitorId}/re-evaluate-health": { + "post": { + "tags": [ + "resource-groups-internal-controller" + ], + "operationId": "reEvaluateGroupHealthForMonitor", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInteger" + } + } + } + } + } + } + }, + "/v1/internal/incidents": { + "post": { + "tags": [ + "incidents-internal-controller" + ], + "operationId": "createAutoIncident", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAutoIncidentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDto" + } + } + } + } + } + } + }, + "/v1/internal/incidents/{id}/resolve": { + "post": { + "tags": [ + "incidents-internal-controller" + ], + "operationId": "resolveAutoIncident", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDto" + } + } + } + } + } + } + }, + "/v1/internal/incidents/{id}/reopen": { + "post": { + "tags": [ + "incidents-internal-controller" + ], + "operationId": "reopenAutoIncident", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReopenAutoIncidentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDto" + } + } + } + } + } + } + }, + "/v1/internal/escalation-tick": { + "post": { + "tags": [ + "escalation-internal-controller" + ], + "operationId": "runEscalationTick", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInteger" + } + } + } + } + } + } + }, + "/v1/internal/billing/sync": { + "post": { + "tags": [ + "admin-billing-controller" + ], + "operationId": "syncFromPaddle", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInteger" + } + } + } + } + } + } + }, + "/v1/internal/adapters/health": { + "get": { + "tags": [ + "adapter-health-internal-controller" + ], + "operationId": "getAllHealth", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultAdapterHealthDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "adapter-health-internal-controller" + ], + "operationId": "reportOutcome", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdapterHealthReportRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAdapterHealthDto" + } + } + } + } + } + } + }, + "/platform/orgs": { + "post": { + "tags": [ + "Organizations" + ], + "summary": "Create organization", + "operationId": "create_1", + "parameters": [ + { + "name": "ifNotExists", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrgRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}/transactions": { + "get": { + "tags": [ + "Transactions" + ], + "summary": "List transactions", + "operationId": "list", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "maximum": 100, + "minimum": 1, + "type": "integer", + "format": "int32", + "default": 10 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTransactionDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Transactions" + ], + "summary": "Create subscription transaction", + "operationId": "createSubscriptionTransaction", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSubscriptionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTransactionDto" + } + } + } + } + } + } + }, + "/platform/onboarding/quick-monitor": { + "post": { + "tags": [ + "Onboarding" + ], + "summary": "Create a monitor with smart defaults from URL analysis", + "operationId": "quickMonitor", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QuickMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + } + }, + "/platform/onboarding/complete-setup": { + "post": { + "tags": [ + "Onboarding" + ], + "summary": "Complete onboarding setup (creates org + workspace, advances to FIRST_MONITOR)", + "operationId": "completeSetup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingSetupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + } + }, + "/platform/onboarding/analyze-url": { + "post": { + "tags": [ + "Onboarding" + ], + "summary": "Analyze a URL and return suggested monitor configuration", + "operationId": "analyzeUrl", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AnalyzeUrlRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAnalyzeUrlResponse" + } + } + } + } + } + } + }, + "/platform/invites/accept": { + "post": { + "tags": [ + "Invites" + ], + "summary": "Accept invite", + "operationId": "accept", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcceptInviteRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAcceptInviteDto" + } + } + } + } + } + } + }, + "/platform/auth/register": { + "post": { + "tags": [ + "Auth" + ], + "summary": "Register user", + "operationId": "register", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterUserRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUserDto" + } + } + } + } + } + } + }, + "/platform/admin/orgs/{orgId}/workspaces": { + "get": { + "tags": [ + "admin-workspace-controller" + ], + "operationId": "listWorkspaces", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWorkspaceDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "admin-workspace-controller" + ], + "operationId": "createWorkspace", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + } + }, + "/platform/admin/orgs/{orgId}/members": { + "get": { + "tags": [ + "admin-member-controller" + ], + "operationId": "listMembers", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMemberDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "admin-member-controller" + ], + "operationId": "addMember", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddMemberRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMemberDto" + } + } + } + } + } + } + }, + "/platform/admin/adapters/{serviceId}/enable": { + "post": { + "tags": [ + "admin-adapter-health-controller" + ], + "operationId": "reEnableAdapter", + "parameters": [ + { + "name": "serviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAdapterHealthDto" + } + } + } + } + } + } + }, + "/api/v1/workspaces": { + "get": { + "tags": [ + "Workspaces" + ], + "summary": "List workspaces", + "operationId": "list_1", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWorkspaceDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Workspaces" + ], + "summary": "Create workspace", + "operationId": "create_2", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWorkspaceRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + } + }, + "/api/v1/webhooks": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "List webhook endpoints for the authenticated org", + "operationId": "list_2", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWebhookEndpointDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Register a new webhook endpoint", + "operationId": "create_3", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWebhookEndpointRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" + } + } + } + } + } + } + }, + "/api/v1/webhooks/{id}/test": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Send a test delivery to a webhook endpoint", + "operationId": "test", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestWebhookEndpointRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWebhookTestResult" + } + } + } + } + } + } + }, + "/api/v1/webhooks/signing-secret/rotate": { + "post": { + "tags": [ + "Webhooks" + ], + "summary": "Generate or rotate the organization webhook signing secret", + "operationId": "rotateSigningSecret", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseString" + } + } + } + } + } + } + }, + "/api/v1/vaults/rotate": { + "post": { + "tags": [ + "Vault" + ], + "summary": "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.", + "operationId": "rotateDek", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDekRotationResultDto" + } + } + } + } + } + } + }, + "/api/v1/tags": { + "get": { + "tags": [ + "Tags" + ], + "summary": "List tags for the authenticated organization", + "operationId": "list_3", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTagDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Tags" + ], + "summary": "Create a new tag", + "operationId": "create_4", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTagRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTagDto" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{slug}": { + "post": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Subscribe to a service or a component of a service", + "description": "Idempotent \u2014 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.", + "operationId": "subscribe", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceSubscribeRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + } + } + } + }, + "/api/v1/secrets": { + "get": { + "tags": [ + "Secrets" + ], + "summary": "List secrets", + "operationId": "list_4", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultSecretDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Secrets" + ], + "summary": "Create secret", + "operationId": "create_5", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSecretRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseSecretDto" + } + } + } + } + } + } + }, + "/api/v1/resource-groups": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "List all resource groups for the authenticated org with health summaries", + "operationId": "list_5", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultResourceGroupDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource Groups" + ], + "summary": "Create a new resource group", + "operationId": "create_6", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateResourceGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" + } + } + } + } + } + } + }, + "/api/v1/resource-groups/{id}/members": { + "post": { + "tags": [ + "Resource Groups" + ], + "summary": "Add a monitor or service member to a resource group", + "operationId": "addMember_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddResourceGroupMemberRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupMemberDto" + } + } + } + } + } + } + }, + "/api/v1/notification-policies": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "List all notification policies for the authenticated org", + "operationId": "list_6", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationPolicyDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Notification Policies" + ], + "summary": "Create a notification policy with match rules and escalation chain", + "operationId": "create_7", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" + } + } + } + } + } + } + }, + "/api/v1/notification-policies/{id}/test": { + "post": { + "tags": [ + "Notification Policies" + ], + "summary": "Dry-run: evaluate a policy's match rules against a supplied incident context", + "operationId": "test_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestNotificationPolicyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTestMatchResult" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches/{id}/acknowledge": { + "post": { + "tags": [ + "Notification Dispatches" + ], + "summary": "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).", + "operationId": "acknowledge", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" + } + } + } + } + } + } + }, + "/api/v1/monitors": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "List monitors for the authenticated org", + "operationId": "list_7", + "parameters": [ + { + "name": "enabled", + "in": "query", + "description": "Filter by enabled state", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "type", + "in": "query", + "description": "Filter by monitor type", + "required": false, + "schema": { + "type": "string", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + } + }, + { + "name": "managedBy", + "in": "query", + "description": "Filter by managed-by source", + "required": false, + "schema": { + "type": "string", + "enum": [ + "DASHBOARD", + "CLI" + ] + } + }, + { + "name": "tags", + "in": "query", + "description": "Filter by tag names, comma-separated (e.g. prod,critical)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "Case-insensitive name search", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "environmentId", + "in": "query", + "description": "Filter by environment ID", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMonitorDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitors" + ], + "summary": "Create a new monitor", + "operationId": "create_8", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMonitorRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{monitorId}/assertions": { + "post": { + "tags": [ + "Monitor Assertions" + ], + "summary": "Add an assertion to a monitor", + "operationId": "add", + "parameters": [ + { + "name": "monitorId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAssertionRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/test": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "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.", + "operationId": "testExisting", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/tags": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get all tags applied to a monitor", + "operationId": "getMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTagDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Monitors" + ], + "summary": "Add tags to a monitor; supports existing tag IDs and inline creation of new tags", + "operationId": "addMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddMonitorTagsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultTagDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Monitors" + ], + "summary": "Remove tags from a monitor by their IDs", + "operationId": "removeMonitorTags", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveMonitorTagsRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/monitors/{id}/rotate-token": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "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.", + "operationId": "rotateToken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/resume": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Resume a monitor (set enabled=true)", + "operationId": "resume", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/pause": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "Pause a monitor (set enabled=false)", + "operationId": "pause", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/test": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "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.", + "operationId": "testAdHoc", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MonitorTestRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/bulk": { + "post": { + "tags": [ + "Monitors" + ], + "summary": "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.", + "operationId": "bulkAction", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkMonitorActionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseBulkMonitorActionResult" + } + } + } + } + } + } + }, + "/api/v1/maintenance-windows": { + "get": { + "tags": [ + "Maintenance Windows" + ], + "summary": "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).", + "operationId": "list_8", + "parameters": [ + { + "name": "monitorId", + "in": "query", + "description": "Filter by monitor UUID", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "filter", + "in": "query", + "description": "Filter by status: 'active' or 'upcoming'", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMaintenanceWindowDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Maintenance Windows" + ], + "summary": "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.", + "operationId": "create_9", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMaintenanceWindowRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" + } + } + } + } + } + } + }, + "/api/v1/invites": { + "get": { + "tags": [ + "Invites" + ], + "summary": "List invites", + "operationId": "list_9", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultInviteDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Invites" + ], + "summary": "Create invite", + "operationId": "create_10", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInviteRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInviteDto" + } + } + } + } + } + } + }, + "/api/v1/invites/{inviteId}/revoke": { + "post": { + "tags": [ + "Invites" + ], + "summary": "Revoke invite", + "operationId": "revoke", + "parameters": [ + { + "name": "inviteId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/invites/{inviteId}/resend": { + "post": { + "tags": [ + "Invites" + ], + "summary": "Resend invite", + "operationId": "resend", + "parameters": [ + { + "name": "inviteId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseInviteDto" + } + } + } + } + } + } + }, + "/api/v1/incidents": { + "get": { + "tags": [ + "Incidents" + ], + "summary": "List incidents for the authenticated org", + "operationId": "list_10", + "parameters": [ + { + "name": "params", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/IncidentFilterParams" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIncidentDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Incidents" + ], + "summary": "Create a manual incident", + "operationId": "create_11", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateManualIncidentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}/updates": { + "post": { + "tags": [ + "Incidents" + ], + "summary": "Add an update to an incident (optionally change status)", + "operationId": "addUpdate", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddIncidentUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}/resolve": { + "post": { + "tags": [ + "Incidents" + ], + "summary": "Resolve an incident", + "operationId": "resolve", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolveIncidentRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + } + } + } + }, + "/api/v1/heartbeat/{token}": { + "get": { + "tags": [ + "Heartbeat" + ], + "summary": "Record a heartbeat ping (GET)", + "description": "Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.", + "operationId": "pingGet", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Ping endpoint token for the heartbeat monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Heartbeat" + ], + "summary": "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.", + "operationId": "pingPost", + "parameters": [ + { + "name": "token", + "in": "path", + "description": "Ping endpoint token for the heartbeat monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "/api/v1/environments": { + "get": { + "tags": [ + "Environments" + ], + "summary": "List environments", + "operationId": "list_11", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultEnvironmentDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Environments" + ], + "summary": "Create environment", + "operationId": "create_12", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEnvironmentRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" + } + } + } + } + } + } + }, + "/api/v1/deploy/lock": { + "get": { + "tags": [ + "Deploy Lock" + ], + "summary": "Get current deploy lock", + "description": "Returns the active deploy lock for the current workspace, if any.", + "operationId": "current", + "parameters": [ + { + "name": "x-phelm-workspace-id", + "in": "header", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Deploy Lock" + ], + "summary": "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.", + "operationId": "acquire", + "parameters": [ + { + "name": "x-phelm-workspace-id", + "in": "header", + "description": "Target workspace ID (defaults to 1)", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcquireDeployLockRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" + } + } + } + } + } + } + }, + "/api/v1/api-keys": { + "get": { + "tags": [ + "API Keys" + ], + "summary": "List API keys", + "operationId": "list_12", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultApiKeyDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "API Keys" + ], + "summary": "Create API key", + "operationId": "create_13", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" + } + } + } + } + } + } + }, + "/api/v1/api-keys/{id}/revoke": { + "post": { + "tags": [ + "API Keys" + ], + "summary": "Revoke API key", + "operationId": "revoke_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" + } + } + } + } + } + } + }, + "/api/v1/api-keys/{id}/regenerate": { + "post": { + "tags": [ + "API Keys" + ], + "summary": "Regenerate API key", + "operationId": "regenerate", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" + } + } + } + } + } + } + }, + "/api/v1/alert-deliveries/{id}/retry": { + "post": { + "tags": [ + "Alert Deliveries" + ], + "summary": "Retry a failed delivery", + "description": "Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.", + "operationId": "retry", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAlertDeliveryDto" + } + } + } + } + } + } + }, + "/api/v1/alert-channels": { + "get": { + "tags": [ + "Alert Channels" + ], + "summary": "List active alert channels for the authenticated org", + "operationId": "list_13", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultAlertChannelDto" + } + } + } + } + } + }, + "post": { + "tags": [ + "Alert Channels" + ], + "summary": "Create a new alert channel with encrypted config", + "operationId": "create_14", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateAlertChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAlertChannelDto" + } + } + } + } + } + } + }, + "/api/v1/alert-channels/{id}/test": { + "post": { + "tags": [ + "Alert Channels" + ], + "summary": "Test a saved alert channel's connectivity", + "operationId": "test_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" + } + } + } + } + } + } + }, + "/api/v1/alert-channels/test": { + "post": { + "tags": [ + "Alert Channels" + ], + "summary": "Test alert channel connectivity using raw config (no saved channel required)", + "operationId": "testConfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestAlertChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" + } + } + } + } + } + } + }, + "/v1/internal/service-incidents/by-ref/{serviceId}/{externalRef}/components": { + "patch": { + "tags": [ + "service-incident-internal-controller" + ], + "operationId": "addComponents", + "parameters": [ + { + "name": "serviceId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "externalRef", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComponentUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIncidentDto" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{id}/alert-sensitivity": { + "patch": { + "tags": [ + "Service Subscriptions" + ], + "summary": "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).", + "operationId": "updateAlertSensitivity", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateAlertSensitivityRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + } + } + } + }, + "/api/v1/api-keys/{id}": { + "delete": { + "tags": [ + "API Keys" + ], + "summary": "Delete API key", + "operationId": "delete_10", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "API Keys" + ], + "summary": "Update API key", + "operationId": "update_14", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" + } + } + } + } + } + } + }, + "/v1/internal/workspaces/{id}": { + "get": { + "tags": [ + "workspaces-controller" + ], + "operationId": "get_7", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" + } + } + } + } + } + } + }, + "/v1/internal/orgs/{id}/workspaces": { + "get": { + "tags": [ + "orgs-controller" + ], + "operationId": "listWorkspaces_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWorkspaceDto" + } + } + } + } + } + } + }, + "/v1/internal/monitors/{id}/policy": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "policy", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" + } + } + } + } + } + } + }, + "/v1/internal/monitors/{id}/env-variables": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "getEnvVariables", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMapStringString" + } + } + } + } + } + } + }, + "/v1/internal/monitors/{id}/auth": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "auth", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" + } + } + } + } + } + } + }, + "/v1/internal/monitors/{id}/assertions": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "getAssertions", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseListMonitorAssertionDto" + } + } + } + } + } + } + }, + "/v1/internal/monitors/{id}/active-incident": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "activeIncident", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDto" + } + } + } + } + } + } + }, + "/v1/internal/monitors/schedulable": { + "get": { + "tags": [ + "monitors-internal-controller" + ], + "operationId": "schedulable", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SchedulableMonitorDto" + } + } + } + } + } + } + } + }, + "/platform/plans": { + "get": { + "tags": [ + "Billing" + ], + "summary": "List public billing plans", + "operationId": "getPublicPlans", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseListBillingPlanDto" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}/subscriptions": { + "get": { + "tags": [ + "Subscriptions" + ], + "summary": "List active subscriptions", + "operationId": "listActive", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultSubscriptionDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Subscriptions" + ], + "operationId": "cancel", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/platform/orgs/{orgId}/subscriptions/upcoming-charge": { + "get": { + "tags": [ + "Subscriptions" + ], + "summary": "Get upcoming charge", + "operationId": "getUpcomingCharge", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "priceId", + "in": "query", + "required": true, + "schema": { + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUpcomingChargeResponse" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}/subscriptions/management-urls": { + "get": { + "tags": [ + "Subscriptions" + ], + "summary": "Get subscription management URLs", + "operationId": "getManagementUrls", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMapStringString" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}/subscriptions/customer-auth-token": { + "get": { + "tags": [ + "Subscriptions" + ], + "summary": "Get customer auth token", + "operationId": "getCustomerAuthToken", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseString" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}/entitlements": { + "get": { + "tags": [ + "Entitlements" + ], + "summary": "Get resolved entitlements and current usage for the organization", + "operationId": "getEntitlements", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseEntitlementResponse" + } + } + } + } + } + } + }, + "/platform/orgs/search": { + "get": { + "tags": [ + "Organizations" + ], + "summary": "Search organizations", + "operationId": "searchOrganizations", + "parameters": [ + { + "name": "query", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "paginationParams", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/PaginationParams" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultIdValuePair" + } + } + } + } + } + } + }, + "/platform/me/orgs": { + "get": { + "tags": [ + "Me" + ], + "summary": "Get current user's organizations", + "operationId": "myOrgs", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMyOrgItemDto" + } + } + } + } + } + } + }, + "/platform/events/stream": { + "get": { + "tags": [ + "Events" + ], + "summary": "Subscribe to real-time platform events via SSE", + "operationId": "stream", + "responses": { + "200": { + "description": "OK", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/SseEmitter" + } + } + } + } + } + } + }, + "/platform/admin/users": { + "get": { + "tags": [ + "admin-controller" + ], + "operationId": "listUsers", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultUserDto" + } + } + } + } + } + } + }, + "/platform/admin/stats": { + "get": { + "tags": [ + "admin-controller" + ], + "operationId": "getStats", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAdminStatsDto" + } + } + } + } + } + } + }, + "/platform/admin/orgs": { + "get": { + "tags": [ + "admin-controller" + ], + "operationId": "listOrgs", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultOrganizationDto" + } + } + } + } + } + } + }, + "/platform/admin/adapters/health": { + "get": { + "tags": [ + "admin-adapter-health-controller" + ], + "operationId": "getAdapterHealth", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultAdapterHealthDto" + } + } + } + } + } + } + }, + "/api/v1/webhooks/{id}/deliveries": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "List recent deliveries for a webhook endpoint", + "operationId": "listDeliveries", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultWebhookDeliveryDto" + } + } + } + } + } + } + }, + "/api/v1/webhooks/signing-secret": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "Get signing secret metadata for the authenticated org", + "operationId": "getSigningSecretInfo", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseWebhookSigningSecretDto" + } + } + } + } + } + } + }, + "/api/v1/webhooks/events": { + "get": { + "tags": [ + "Webhooks" + ], + "summary": "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.", + "operationId": "listEvents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/WebhookEventCatalogResponse" + } + } + } + } + } + } + }, + "/api/v1/services": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List all enabled services (cursor-paginated)", + "operationId": "listServices", + "parameters": [ + { + "name": "category", + "in": "query", + "description": "Filter by category (exact match)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter by current overall_status (exact match)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor from a previous response", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Page size (1\u2013100, default 20)", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageServiceCatalogDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get a single service by slug or UUID with current status, components, and recent incidents", + "operationId": "getService", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceDetailDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/uptime": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get uptime statistics for a service", + "description": "Uptime data aggregated across active non-group components.", + "operationId": "getServiceUptime", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "period", + "in": "query", + "description": "Time window", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d", + "1y", + "2y", + "all" + ] + } + }, + { + "name": "granularity", + "in": "query", + "description": "Bucket granularity", + "required": false, + "schema": { + "type": "string", + "enum": [ + "hourly", + "daily", + "monthly" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceUptimeResponse" + } + } + } + } + }, + "security": [ + { + "BearerAuth": [] + } + ] + } + }, + "/api/v1/services/{slugOrId}/maintenances": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List scheduled maintenances for a service", + "operationId": "getScheduledMaintenances", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter by status (e.g. scheduled, in_progress, verifying, completed)", + "required": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultScheduledMaintenanceDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/incidents": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List incident history for a service (paginated)", + "operationId": "listIncidents", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "description": "Earliest start date (ISO 8601 date)", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter: active (unresolved), resolved, or omit for all", + "required": false, + "schema": { + "type": "string", + "enum": [ + "active", + "resolved" + ] + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/incidents/{incidentId}": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get incident detail with full update timeline", + "operationId": "getIncident", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "incidentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceIncidentDetailDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/components": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List active components for a service with current status and inline uptime", + "operationId": "getComponents", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceComponentDto" + } + } + } + } + } + } + }, + "/api/v1/services/{slugOrId}/components/{componentId}/uptime": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Get daily uptime data for a component", + "operationId": "getComponentUptime", + "parameters": [ + { + "name": "slugOrId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "componentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "period", + "in": "query", + "description": "Time window", + "required": false, + "schema": { + "type": "string", + "enum": [ + "7d", + "30d", + "90d", + "1y" + ] + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultComponentUptimeDayDto" + } + } + } + } + } + } + }, + "/api/v1/services/summary": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "Global status summary across all services", + "description": "Returns aggregate counts of services by status and a list of services currently experiencing issues.", + "operationId": "getGlobalStatusSummary", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseGlobalStatusSummaryDto" + } + } + } + } + } + } + }, + "/api/v1/services/incidents": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List vendor incidents across all services (paginated)", + "description": "Cross-service vendor incident feed ordered by start date descending.", + "operationId": "listCrossServiceIncidents", + "parameters": [ + { + "name": "from", + "in": "query", + "description": "Earliest start date (ISO 8601 date)", + "required": false, + "schema": { + "type": "string", + "format": "date" + } + }, + { + "name": "status", + "in": "query", + "description": "Filter: active (unresolved), resolved, or omit for all", + "required": false, + "schema": { + "type": "string", + "enum": [ + "active", + "resolved" + ] + } + }, + { + "name": "category", + "in": "query", + "description": "Filter by service category", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions": { + "get": { + "tags": [ + "Service Subscriptions" + ], + "summary": "List all service subscriptions for the organization", + "operationId": "list_14", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultServiceSubscriptionDto" + } + } + } + } + } + } + }, + "/api/v1/service-subscriptions/{id}": { + "get": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Get a subscription by its ID", + "operationId": "get_8", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Service Subscriptions" + ], + "summary": "Remove a subscription by its ID", + "description": "Removes a specific subscription (whole-service or component-level). No-op if not found.", + "operationId": "unsubscribe", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/resource-groups/{id}/health": { + "get": { + "tags": [ + "Resource Groups" + ], + "summary": "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.", + "operationId": "getHealth", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResourceGroupHealthDto" + } + } + } + } + } + } + }, + "/api/v1/notifications": { + "get": { + "tags": [ + "Notifications" + ], + "summary": "List notifications for the current user", + "operationId": "list_15", + "parameters": [ + { + "name": "unreadOnly", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDto" + } + } + } + } + } + } + }, + "/api/v1/notifications/unread-count": { + "get": { + "tags": [ + "Notifications" + ], + "summary": "Get unread notification count", + "operationId": "unreadCount", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseLong" + } + } + } + } + } + } + }, + "/api/v1/notification-policies/{id}/dispatches": { + "get": { + "tags": [ + "Notification Policies" + ], + "summary": "List all dispatches (firing history) for a notification policy", + "operationId": "listDispatches", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches": { + "get": { + "tags": [ + "Notification Dispatches" + ], + "summary": "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.", + "operationId": "listByIncident", + "parameters": [ + { + "name": "incident_id", + "in": "query", + "description": "UUID of the incident to inspect", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" + } + } + } + } + } + } + }, + "/api/v1/notification-dispatches/{id}": { + "get": { + "tags": [ + "Notification Dispatches" + ], + "summary": "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.", + "operationId": "getById_2", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/versions": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "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.", + "operationId": "listVersions", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMonitorVersionDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/versions/{version}": { + "get": { + "tags": [ + "Monitors" + ], + "summary": "Get a specific version snapshot for a monitor", + "description": "Returns the full monitor config snapshot captured at the given version number.", + "operationId": "getVersion", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseMonitorVersionDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/uptime": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "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.", + "operationId": "getUptime", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "window", + "in": "query", + "description": "Time window for uptime calculation", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d" + ] + } + } + ], + "responses": { + "200": { + "description": "Uptime statistics", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UptimeDto" + } + } + } + }, + "400": { + "description": "Invalid window parameter", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseUptimeDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/results": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "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 \u2014 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.", + "operationId": "getResults", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "from", + "in": "query", + "description": "Start of time range (ISO 8601, inclusive); defaults to 24 hours ago", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "description": "End of time range (ISO 8601, inclusive); defaults to now", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "cursor", + "in": "query", + "description": "Opaque cursor from a previous response for pagination", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum results per page (1\u2013200)", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + }, + "example": 50 + }, + { + "name": "region", + "in": "query", + "description": "Filter by region (e.g. us-east)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "passed", + "in": "query", + "description": "Filter by pass/fail status", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Paginated check results", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPage" + } + } + } + }, + "400": { + "description": "Invalid query parameters", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CursorPageCheckResultDto" + } + } + } + } + } + } + }, + "/api/v1/monitors/{id}/results/summary": { + "get": { + "tags": [ + "Check Results" + ], + "summary": "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.", + "operationId": "getSummary", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "chartWindow", + "in": "query", + "description": "Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets", + "required": false, + "schema": { + "type": "string", + "enum": [ + "24h", + "7d", + "30d", + "90d" + ] + } + } + ], + "responses": { + "200": { + "description": "Results summary", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ResultSummaryDto" + } + } + } + }, + "400": { + "description": "Invalid chartWindow parameter", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + }, + "403": { + "description": "Monitor does not belong to the caller's org", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + }, + "404": { + "description": "Monitor not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" + } + } + } + } + } + } + }, + "/api/v1/members": { + "get": { + "tags": [ + "Members" + ], + "summary": "List organization members", + "operationId": "list_16", + "parameters": [ + { + "name": "pageable", + "in": "query", + "required": true, + "schema": { + "$ref": "#/components/schemas/Pageable" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultMemberDto" + } + } + } + } + } + } + }, + "/api/v1/integrations": { + "get": { + "tags": [ + "Integrations" + ], + "summary": "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.", + "operationId": "list_17", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IntegrationCatalogResponse" + } + } + } + } + } + } + }, + "/api/v1/incidents/{id}": { + "get": { + "tags": [ + "Incidents" + ], + "summary": "Get incident details including update timeline", + "operationId": "get_9", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" + } + } + } + } + } + } + }, + "/api/v1/dashboard/overview": { + "get": { + "tags": [ + "Dashboard" + ], + "summary": "Dashboard overview", + "description": "Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.", + "operationId": "overview", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseDashboardOverviewDto" + } + } + } + } + } + } + }, + "/api/v1/categories": { + "get": { + "tags": [ + "Status Data" + ], + "summary": "List categories with service counts", + "operationId": "listCategories", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultCategoryDto" + } + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "tags": [ + "API Auth" + ], + "summary": "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_...).", + "operationId": "me_1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SingleValueResponseAuthMeResponse" + } + } + } + } + } + } + }, + "/api/v1/audit-log": { + "get": { + "tags": [ + "Audit Log" + ], + "summary": "List audit events for the current organization", + "operationId": "list_18", + "parameters": [ + { + "name": "action", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "actorId", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "resourceType", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PageResultAuditEventDto" + } + } + } + } + } + } + }, + "/api/v1/alert-deliveries/{id}/attempts": { + "get": { + "tags": [ + "Alert Deliveries" + ], + "summary": "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.", + "operationId": "listAttempts", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultDeliveryAttemptDto" + } + } + } + } + } + } + }, + "/api/v1/alert-channels/{id}/deliveries": { + "get": { + "tags": [ + "Alert Channels" + ], + "summary": "List delivery history for an alert channel", + "operationId": "listDeliveries_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TableValueResultAlertDeliveryDto" + } + } + } + } + } + } + }, + "/platform/orgs/{orgId}": { + "delete": { + "tags": [ + "Organizations" + ], + "summary": "Delete organization", + "operationId": "delete_11", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/platform/admin/orgs/{orgId}/members/{userId}": { + "delete": { + "tags": [ + "admin-member-controller" + ], + "operationId": "removeMember", + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/resource-groups/{id}/members/{memberId}": { + "delete": { + "tags": [ + "Resource Groups" + ], + "summary": "Remove a member from a resource group", + "operationId": "removeMember_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "memberId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/members/{userId}": { + "delete": { + "tags": [ + "Members" + ], + "summary": "Remove member from organization", + "operationId": "remove_2", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/deploy/lock/{lockId}": { + "delete": { + "tags": [ + "Deploy Lock" + ], + "summary": "Release deploy lock", + "description": "Releases a deploy lock by ID. Only the lock holder should call this.", + "operationId": "release", + "parameters": [ + { + "name": "lockId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "x-phelm-workspace-id", + "in": "header", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/api/v1/deploy/lock/force": { + "delete": { + "tags": [ + "Deploy Lock" + ], + "summary": "Force-release deploy lock", + "description": "Forcibly removes any deploy lock on the current workspace. Use to break stale locks.", + "operationId": "forceRelease", + "parameters": [ + { + "name": "x-phelm-workspace-id", + "in": "header", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "CreateSubscriptionRequest": { + "type": "object", + "properties": { + "priceId": { + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + }, + "BillingPlanDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "paddleId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "prices": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/BillingPriceDto" + } + } + }, + "nullable": true + }, + "BillingPriceDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "paddleId": { + "type": "string" + }, + "amount": { + "type": "integer", + "format": "int32" + }, + "interval": { + "type": "string", + "enum": [ + "DAY", + "WEEK", + "MONTH", + "YEAR" + ] + }, + "intervalCount": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + }, + "billingPlan": { + "$ref": "#/components/schemas/BillingPlanDto" + } + } + }, + "ItemDto": { + "type": "object", + "properties": { + "billingPrice": { + "$ref": "#/components/schemas/BillingPriceDto" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "amount": { + "type": "integer", + "format": "int32" + } + } + }, + "SingleValueResponseSubscriptionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SubscriptionDto" + } + } + }, + "SubscriptionDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "paddleId": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "CANCELED", + "PAST_DUE", + "PAUSED", + "TRIALING" + ] + }, + "nextBilledAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "willCancelAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ItemDto" + } + } + } + }, + "UpdateOrgDetailsRequest": { + "required": [ + "email", + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string" + }, + "email": { + "minLength": 1, + "type": "string", + "format": "email" + }, + "size": { + "maxLength": 50, + "minLength": 0, + "type": "string" + }, + "industry": { + "maxLength": 100, + "minLength": 0, + "type": "string" + }, + "websiteUrl": { + "maxLength": 255, + "minLength": 0, + "type": "string" + } + } + }, + "OrganizationDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string", + "nullable": true + }, + "size": { + "type": "string", + "nullable": true + }, + "industry": { + "type": "string", + "nullable": true + }, + "websiteUrl": { + "type": "string", + "nullable": true + } + } + }, + "SingleValueResponseOrganizationDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationDto" + } + } + }, + "UpdateOnboardingStageRequest": { + "required": [ + "stage" + ], + "type": "object", + "properties": { + "stage": { + "type": "string", + "enum": [ + "WELCOME", + "FIRST_MONITOR", + "SETUP_COMPLETE", + "COMPLETED" + ] + } + } + }, + "SingleValueResponseUserDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/UserDto" + } + } + }, + "UserDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + }, + "name": { + "type": "string", + "nullable": true + }, + "userRole": { + "type": "string", + "enum": [ + "SUPERADMIN", + "ADMIN", + "USER" + ] + }, + "onboardingStage": { + "type": "string", + "nullable": true, + "enum": [ + "WELCOME", + "FIRST_MONITOR", + "SETUP_COMPLETE", + "COMPLETED" + ] + }, + "imageUrl": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "UpdateProfileRequest": { + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string" + } + } + }, + "UpdateNotificationPreferencesRequest": { + "required": [ + "preferences" + ], + "type": "object", + "properties": { + "preferences": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + }, + "NotificationPreferencesDto": { + "type": "object", + "properties": { + "preferences": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseNotificationPreferencesDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NotificationPreferencesDto" + } + } + }, + "UpdateWorkspaceRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string" + } + } + }, + "SingleValueResponseWorkspaceDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/WorkspaceDto" + } + } + }, + "WorkspaceDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "orgId": { + "type": "integer", + "format": "int32" + } + } + }, + "UpdateUserRequest": { + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "userRole": { + "type": "string", + "enum": [ + "SUPERADMIN", + "ADMIN", + "USER" + ] + }, + "onboardingStage": { + "type": "string", + "enum": [ + "WELCOME", + "FIRST_MONITOR", + "SETUP_COMPLETE", + "COMPLETED" + ] + }, + "imageUrl": { + "maxLength": 500, + "minLength": 0, + "type": "string" + } + } + }, + "ChangeRoleRequest": { + "required": [ + "orgRole" + ], + "type": "object", + "properties": { + "orgRole": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + } + } + }, + "UpdateWebhookEndpointRequest": { + "type": "object", + "properties": { + "url": { + "maxLength": 2048, + "minLength": 0, + "type": "string", + "description": "New webhook URL; null preserves current", + "nullable": true + }, + "description": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "New description; null preserves current", + "nullable": true + }, + "subscribedEvents": { + "type": "array", + "description": "Replace subscribed events; null preserves current", + "nullable": true, + "items": { + "type": "string", + "description": "Replace subscribed events; null preserves current", + "nullable": true + } + }, + "enabled": { + "type": "boolean", + "description": "Enable or disable delivery; null preserves current", + "nullable": true + } + } + }, + "SingleValueResponseWebhookEndpointDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/WebhookEndpointDto" + } + } + }, + "WebhookEndpointDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "url": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "subscribedEvents": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + }, + "consecutiveFailures": { + "type": "integer", + "format": "int32" + }, + "disabledReason": { + "type": "string", + "nullable": true + }, + "disabledAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "UpdateTagRequest": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "minLength": 0, + "type": "string", + "description": "New tag name", + "nullable": true + }, + "color": { + "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", + "type": "string", + "description": "New hex color code", + "nullable": true + } + }, + "description": "Request body for updating a tag; null fields are left unchanged" + }, + "SingleValueResponseTagDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TagDto" + } + } + }, + "TagDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "UpdateSecretRequest": { + "required": [ + "value" + ], + "type": "object", + "properties": { + "value": { + "maxLength": 32768, + "minLength": 0, + "type": "string", + "description": "New secret value, stored encrypted (max 32KB)" + } + } + }, + "MonitorReference": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + } + }, + "SecretDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "key": { + "type": "string" + }, + "dekVersion": { + "type": "integer", + "format": "int32" + }, + "valueHash": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "usedByMonitors": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/MonitorReference" + } + } + } + }, + "SingleValueResponseSecretDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SecretDto" + } + } + }, + "RetryStrategy": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "maxRetries": { + "type": "integer", + "format": "int32" + }, + "interval": { + "type": "integer", + "format": "int32" + } + }, + "description": "Default retry strategy for member monitors; null clears" + }, + "UpdateResourceGroupRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this group" + }, + "description": { + "type": "string", + "description": "Optional description; null clears the existing value", + "nullable": true + }, + "alertPolicyId": { + "type": "string", + "description": "Optional notification policy to apply for this group; null clears the existing value", + "format": "uuid", + "nullable": true + }, + "defaultFrequency": { + "maximum": 86400, + "minimum": 30, + "type": "integer", + "description": "Default check frequency in seconds for members (30\u201386400); null clears", + "format": "int32", + "nullable": true + }, + "defaultRegions": { + "type": "array", + "description": "Default regions for member monitors; null clears", + "nullable": true, + "items": { + "type": "string", + "description": "Default regions for member monitors; null clears", + "nullable": true + } + }, + "defaultRetryStrategy": { + "$ref": "#/components/schemas/RetryStrategy" + }, + "defaultAlertChannels": { + "type": "array", + "description": "Default alert channel IDs for member monitors; null clears", + "nullable": true, + "items": { + "type": "string", + "description": "Default alert channel IDs for member monitors; null clears", + "format": "uuid", + "nullable": true + } + }, + "defaultEnvironmentId": { + "type": "string", + "description": "Default environment ID for member monitors; null clears", + "format": "uuid", + "nullable": true + }, + "healthThresholdType": { + "type": "string", + "description": "Health threshold type: COUNT or PERCENTAGE; null disables threshold", + "nullable": true, + "enum": [ + "COUNT", + "PERCENTAGE" + ] + }, + "healthThresholdValue": { + "maximum": 100, + "exclusiveMaximum": false, + "minimum": 0, + "exclusiveMinimum": false, + "type": "number", + "description": "Health threshold value; null disables threshold", + "nullable": true + }, + "suppressMemberAlerts": { + "type": "boolean", + "description": "Suppress member-level alert notifications; null preserves current value", + "nullable": true + }, + "confirmationDelaySeconds": { + "maximum": 600, + "minimum": 0, + "type": "integer", + "description": "Confirmation delay in seconds; null clears", + "format": "int32", + "nullable": true + }, + "recoveryCooldownMinutes": { + "maximum": 60, + "minimum": 0, + "type": "integer", + "description": "Recovery cooldown in minutes; null clears", + "format": "int32", + "nullable": true + } + }, + "description": "Request body for updating a resource group" + }, + "ResourceGroupDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "alertPolicyId": { + "type": "string", + "description": "Notification policy applied to this group", + "format": "uuid", + "nullable": true + }, + "defaultFrequency": { + "type": "integer", + "description": "Default check frequency in seconds for member monitors", + "format": "int32", + "nullable": true + }, + "defaultRegions": { + "type": "array", + "description": "Default regions for member monitors", + "nullable": true, + "items": { + "type": "string", + "description": "Default regions for member monitors", + "nullable": true + } + }, + "defaultRetryStrategy": { + "$ref": "#/components/schemas/RetryStrategy" + }, + "defaultAlertChannels": { + "type": "array", + "description": "Default alert channel IDs for member monitors", + "nullable": true, + "items": { + "type": "string", + "description": "Default alert channel IDs for member monitors", + "format": "uuid", + "nullable": true + } + }, + "defaultEnvironmentId": { + "type": "string", + "description": "Default environment ID for member monitors", + "format": "uuid", + "nullable": true + }, + "healthThresholdType": { + "type": "string", + "description": "Health threshold type: COUNT or PERCENTAGE", + "nullable": true, + "enum": [ + "COUNT", + "PERCENTAGE" + ] + }, + "healthThresholdValue": { + "type": "number", + "description": "Health threshold value", + "nullable": true + }, + "suppressMemberAlerts": { + "type": "boolean", + "description": "When true, member-level incidents skip notification dispatch; only group alerts fire" + }, + "confirmationDelaySeconds": { + "type": "integer", + "description": "Seconds to wait after health threshold breach before creating group incident", + "format": "int32", + "nullable": true + }, + "recoveryCooldownMinutes": { + "type": "integer", + "description": "Cooldown minutes after group incident resolves before a new one can open", + "format": "int32", + "nullable": true + }, + "health": { + "$ref": "#/components/schemas/ResourceGroupHealthDto" + }, + "members": { + "type": "array", + "description": "Member list with individual statuses; populated on detail GET only", + "nullable": true, + "items": { + "$ref": "#/components/schemas/ResourceGroupMemberDto" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Resource group with health summary and optional member details" + }, + "ResourceGroupHealthDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Worst-of health status across all members", + "enum": [ + "operational", + "maintenance", + "degraded", + "down" + ] + }, + "totalMembers": { + "type": "integer", + "description": "Total number of members in the group", + "format": "int32" + }, + "operationalCount": { + "type": "integer", + "description": "Number of members currently in operational status", + "format": "int32" + }, + "activeIncidents": { + "type": "integer", + "description": "Number of members with an active incident or non-operational status", + "format": "int32" + }, + "thresholdStatus": { + "type": "string", + "description": "Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured.", + "nullable": true, + "enum": [ + "healthy", + "degraded", + "down" + ] + }, + "failingCount": { + "type": "integer", + "description": "Number of failing members at time of last evaluation", + "format": "int32", + "nullable": true + } + }, + "description": "Aggregated health summary for a resource group" + }, + "ResourceGroupMemberDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "groupId": { + "type": "string", + "format": "uuid" + }, + "memberType": { + "type": "string", + "description": "Type of member: 'monitor' or 'service'" + }, + "monitorId": { + "type": "string", + "description": "Monitor ID; set when memberType is 'monitor'", + "format": "uuid", + "nullable": true + }, + "serviceId": { + "type": "string", + "description": "Service ID; set when memberType is 'service'", + "format": "uuid", + "nullable": true + }, + "name": { + "type": "string", + "description": "Display name of the referenced monitor or service", + "nullable": true + }, + "slug": { + "type": "string", + "description": "Slug identifier for the service (services only); used for icons and uptime API calls", + "nullable": true + }, + "subscriptionId": { + "type": "string", + "description": "Subscription ID for the service (services only); used to link to the dependency detail page", + "format": "uuid", + "nullable": true + }, + "status": { + "type": "string", + "description": "Computed health status for this member", + "enum": [ + "operational", + "maintenance", + "degraded", + "down" + ] + }, + "effectiveFrequency": { + "type": "string", + "description": "Effective check frequency label showing the group default when the monitor inherits it; null for services or when no group default is configured", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "uptime24h": { + "type": "number", + "description": "24h uptime percentage; populated when includeMetrics=true", + "format": "double", + "nullable": true + }, + "chartData": { + "type": "array", + "description": "Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true", + "nullable": true, + "items": { + "type": "number", + "description": "Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true", + "format": "double", + "nullable": true + } + }, + "avgLatencyMs": { + "type": "number", + "description": "Average latency in ms (monitors only); populated when includeMetrics=true", + "format": "double", + "nullable": true + }, + "p95LatencyMs": { + "type": "number", + "description": "P95 latency in ms (monitors only); populated when includeMetrics=true", + "format": "double", + "nullable": true + }, + "lastCheckedAt": { + "type": "string", + "description": "Timestamp of the most recent health check; populated when includeMetrics=true", + "format": "date-time", + "nullable": true + }, + "monitorType": { + "type": "string", + "description": "Monitor type (HTTP, DNS, TCP, ICMP, HEARTBEAT, MCP); monitors only", + "nullable": true + }, + "environmentName": { + "type": "string", + "description": "Environment name; monitors only", + "nullable": true + } + }, + "description": "A single member of a resource group with its computed health status" + }, + "SingleValueResponseResourceGroupDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ResourceGroupDto" + } + } + }, + "EscalationChain": { + "required": [ + "steps" + ], + "type": "object", + "properties": { + "steps": { + "minItems": 1, + "type": "array", + "description": "Ordered escalation steps, evaluated in sequence", + "items": { + "$ref": "#/components/schemas/EscalationStep" + } + }, + "onResolve": { + "type": "string", + "description": "Action when the incident resolves", + "nullable": true + }, + "onReopen": { + "type": "string", + "description": "Action when a resolved incident reopens", + "nullable": true + } + }, + "description": "Escalation chain defining which channels to notify" + }, + "EscalationStep": { + "required": [ + "channelIds" + ], + "type": "object", + "properties": { + "delayMinutes": { + "minimum": 0, + "type": "integer", + "description": "Minutes to wait before executing this step (0 = immediate)", + "format": "int32" + }, + "channelIds": { + "minItems": 1, + "type": "array", + "description": "Alert channel IDs to notify in this step", + "items": { + "type": "string", + "description": "Alert channel IDs to notify in this step", + "format": "uuid" + } + }, + "requireAck": { + "type": "boolean", + "description": "Whether an acknowledgment is required before escalating", + "nullable": true + }, + "repeatIntervalSeconds": { + "minimum": 1, + "type": "integer", + "description": "Repeat notification interval in seconds until acknowledged", + "format": "int32", + "nullable": true + } + }, + "description": "Ordered escalation steps, evaluated in sequence" + }, + "MatchRule": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Rule type, e.g. severity_gte, monitor_id_in, region_in" + }, + "value": { + "type": "string", + "description": "Comparison value for single-value rules like severity_gte", + "nullable": true + }, + "monitorIds": { + "type": "array", + "description": "Monitor UUIDs to match for monitor_id_in rules", + "nullable": true, + "items": { + "type": "string", + "description": "Monitor UUIDs to match for monitor_id_in rules", + "format": "uuid", + "nullable": true + } + }, + "regions": { + "type": "array", + "description": "Region codes to match for region_in rules", + "nullable": true, + "items": { + "type": "string", + "description": "Region codes to match for region_in rules", + "nullable": true + } + }, + "values": { + "type": "array", + "description": "Values list for multi-value rules like monitor_type_in", + "nullable": true, + "items": { + "type": "string", + "description": "Values list for multi-value rules like monitor_type_in", + "nullable": true + } + } + }, + "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)" + }, + "UpdateNotificationPolicyRequest": { + "required": [ + "enabled", + "escalation", + "name", + "priority" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this policy" + }, + "matchRules": { + "type": "array", + "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)", + "items": { + "$ref": "#/components/schemas/MatchRule" + } + }, + "escalation": { + "$ref": "#/components/schemas/EscalationChain" + }, + "enabled": { + "type": "boolean", + "description": "Whether this policy is enabled" + }, + "priority": { + "type": "integer", + "description": "Evaluation priority; higher value = evaluated first", + "format": "int32" + } + }, + "description": "Request body for updating a notification policy" + }, + "NotificationPolicyDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string", + "description": "Human-readable name for this policy" + }, + "matchRules": { + "type": "array", + "description": "Match rules (all must pass; empty = catch-all)", + "items": { + "$ref": "#/components/schemas/MatchRule" + } + }, + "escalation": { + "$ref": "#/components/schemas/EscalationChain" + }, + "enabled": { + "type": "boolean", + "description": "Whether this policy is active" + }, + "priority": { + "type": "integer", + "description": "Evaluation order; higher value = evaluated first", + "format": "int32" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Org-level notification policy with match rules and escalation chain" + }, + "SingleValueResponseNotificationPolicyDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NotificationPolicyDto" + } + } + }, + "ConfirmationPolicy": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "multi_region" + ] + }, + "minRegionsFailing": { + "type": "integer", + "format": "int32" + }, + "maxWaitSeconds": { + "type": "integer", + "format": "int32" + } + }, + "description": "Multi-region confirmation settings" + }, + "IncidentPolicyDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid" + }, + "triggerRules": { + "type": "array", + "description": "Array of trigger rules defining when an incident should be raised", + "items": { + "$ref": "#/components/schemas/TriggerRule" + } + }, + "confirmation": { + "$ref": "#/components/schemas/ConfirmationPolicy" + }, + "recovery": { + "$ref": "#/components/schemas/RecoveryPolicy" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "monitorRegionCount": { + "type": "integer", + "description": "Number of regions configured on the monitor (only set in internal API responses)", + "format": "int32", + "nullable": true + }, + "checkFrequencySeconds": { + "type": "integer", + "description": "Monitor check frequency in seconds (only set in internal API responses)", + "format": "int32", + "nullable": true + } + }, + "description": "Incident detection, confirmation, and recovery policy for a monitor" + }, + "RecoveryPolicy": { + "type": "object", + "properties": { + "consecutiveSuccesses": { + "type": "integer", + "format": "int32" + }, + "minRegionsPassing": { + "type": "integer", + "format": "int32" + }, + "cooldownMinutes": { + "type": "integer", + "format": "int32" + } + }, + "description": "Auto-recovery settings" + }, + "TriggerRule": { + "required": [ + "scope", + "severity", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "consecutive_failures", + "failures_in_window", + "response_time" + ] + }, + "count": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "windowMinutes": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "scope": { + "type": "string", + "nullable": true, + "enum": [ + "per_region", + "any_region" + ] + }, + "thresholdMs": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "severity": { + "type": "string", + "enum": [ + "down", + "degraded" + ] + }, + "aggregationType": { + "type": "string", + "nullable": true, + "enum": [ + "all_exceed", + "average", + "p95", + "max" + ] + } + }, + "description": "Array of trigger rules defining when an incident should be raised" + }, + "UpdateIncidentPolicyRequest": { + "required": [ + "confirmation", + "recovery", + "triggerRules" + ], + "type": "object", + "properties": { + "triggerRules": { + "minItems": 1, + "type": "array", + "description": "Array of trigger rules; at least one required", + "items": { + "$ref": "#/components/schemas/TriggerRule" + } + }, + "confirmation": { + "$ref": "#/components/schemas/ConfirmationPolicy" + }, + "recovery": { + "$ref": "#/components/schemas/RecoveryPolicy" + } + }, + "description": "Request body for updating an incident policy" + }, + "SingleValueResponseIncidentPolicyDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/IncidentPolicyDto" + } + } + }, + "ApiKeyAuthConfig": { + "required": [ + "headerName" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorAuthConfig" + }, + { + "type": "object", + "properties": { + "headerName": { + "minLength": 1, + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + }, + "vaultSecretId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + ] + }, + "BasicAuthConfig": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorAuthConfig" + }, + { + "type": "object", + "properties": { + "vaultSecretId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + ] + }, + "BearerAuthConfig": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorAuthConfig" + }, + { + "type": "object", + "properties": { + "vaultSecretId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + ] + }, + "HeaderAuthConfig": { + "required": [ + "headerName" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorAuthConfig" + }, + { + "type": "object", + "properties": { + "headerName": { + "minLength": 1, + "pattern": "^[A-Za-z0-9\\-_]+$", + "type": "string" + }, + "vaultSecretId": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + ] + }, + "MonitorAuthConfig": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "type" + } + }, + "UpdateMonitorAuthRequest": { + "required": [ + "config" + ], + "type": "object", + "properties": { + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + } + } + }, + "MonitorAuthDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid" + }, + "authType": { + "type": "string", + "enum": [ + "bearer", + "basic", + "header", + "api_key" + ] + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + } + } + }, + "SingleValueResponseMonitorAuthDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MonitorAuthDto" + } + } + }, + "AssertionConfig": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "type" + } + }, + "BodyContainsAssertion": { + "required": [ + "substring" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "substring": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "DnsExpectedCnameAssertion": { + "required": [ + "value" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "value": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "DnsExpectedIpsAssertion": { + "required": [ + "ips" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "ips": { + "minItems": 1, + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, + "DnsMaxAnswersAssertion": { + "required": [ + "recordType" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "recordType": { + "minLength": 1, + "type": "string" + }, + "max": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsMinAnswersAssertion": { + "required": [ + "recordType" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "recordType": { + "minLength": 1, + "type": "string" + }, + "min": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsRecordContainsAssertion": { + "required": [ + "recordType", + "substring" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "recordType": { + "minLength": 1, + "type": "string" + }, + "substring": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "DnsRecordEqualsAssertion": { + "required": [ + "recordType", + "value" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "recordType": { + "minLength": 1, + "type": "string" + }, + "value": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "DnsResolvesAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + } + ] + }, + "DnsResponseTimeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsResponseTimeWarnAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "warnMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsTtlHighAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxTtl": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsTtlLowAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "minTtl": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "DnsTxtContainsAssertion": { + "required": [ + "substring" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "substring": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "HeaderValueAssertion": { + "required": [ + "expected", + "headerName", + "operator" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "headerName": { + "minLength": 1, + "type": "string" + }, + "expected": { + "minLength": 1, + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" + ] + } + } + } + ] + }, + "HeartbeatIntervalDriftAssertion": { + "required": [ + "maxDeviationPercent" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxDeviationPercent": { + "maximum": 100, + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "HeartbeatMaxIntervalAssertion": { + "required": [ + "maxSeconds" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxSeconds": { + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "HeartbeatPayloadContainsAssertion": { + "required": [ + "path", + "value" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "path": { + "minLength": 1, + "type": "string" + }, + "value": { + "type": "string" + } + } + } + ] + }, + "HeartbeatReceivedAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + } + ] + }, + "IcmpPacketLossAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxPercent": { + "maximum": 100.0, + "exclusiveMaximum": false, + "minimum": 0.0, + "exclusiveMinimum": false, + "type": "number", + "format": "double" + } + } + } + ] + }, + "IcmpReachableAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + } + ] + }, + "IcmpResponseTimeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "IcmpResponseTimeWarnAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "warnMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "JsonPathAssertion": { + "required": [ + "expected", + "operator", + "path" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "path": { + "minLength": 1, + "type": "string" + }, + "expected": { + "minLength": 1, + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" + ] + } + } + } + ] + }, + "McpConnectsAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + } + ] + }, + "McpHasCapabilityAssertion": { + "required": [ + "capability" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "capability": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "McpMinToolsAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "min": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "McpProtocolVersionAssertion": { + "required": [ + "version" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "version": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "McpResponseTimeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "McpResponseTimeWarnAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "warnMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "McpToolAvailableAssertion": { + "required": [ + "toolName" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "toolName": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "McpToolCountChangedAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "expectedCount": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "RedirectCountAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxCount": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "RedirectTargetAssertion": { + "required": [ + "expected", + "operator" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "expected": { + "minLength": 1, + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" + ] + } + } + } + ] + }, + "RegexBodyAssertion": { + "required": [ + "pattern" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "pattern": { + "minLength": 1, + "type": "string" + } + } + } + ] + }, + "ResponseSizeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxBytes": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "ResponseTimeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "thresholdMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "ResponseTimeWarnAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "warnMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "SslExpiryAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "minDaysRemaining": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "StatusCodeAssertion": { + "required": [ + "expected", + "operator" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "expected": { + "minLength": 1, + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "equals", + "contains", + "less_than", + "greater_than", + "matches", + "range" + ] + } + } + } + ] + }, + "TcpConnectsAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + } + ] + }, + "TcpResponseTimeAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "maxMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "TcpResponseTimeWarnAssertion": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/AssertionConfig" + }, + { + "type": "object", + "properties": { + "warnMs": { + "type": "integer", + "format": "int32" + } + } + } + ] + }, + "UpdateAssertionRequest": { + "required": [ + "config" + ], + "type": "object", + "properties": { + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/BodyContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedCnameAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedIpsAssertion" + }, + { + "$ref": "#/components/schemas/DnsMaxAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsMinAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordEqualsAssertion" + }, + { + "$ref": "#/components/schemas/DnsResolvesAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlHighAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlLowAssertion" + }, + { + "$ref": "#/components/schemas/DnsTxtContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeaderValueAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatReceivedAssertion" + }, + { + "$ref": "#/components/schemas/IcmpPacketLossAssertion" + }, + { + "$ref": "#/components/schemas/IcmpReachableAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/JsonPathAssertion" + }, + { + "$ref": "#/components/schemas/McpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/McpHasCapabilityAssertion" + }, + { + "$ref": "#/components/schemas/McpMinToolsAssertion" + }, + { + "$ref": "#/components/schemas/McpProtocolVersionAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/McpToolAvailableAssertion" + }, + { + "$ref": "#/components/schemas/McpToolCountChangedAssertion" + }, + { + "$ref": "#/components/schemas/RedirectCountAssertion" + }, + { + "$ref": "#/components/schemas/RedirectTargetAssertion" + }, + { + "$ref": "#/components/schemas/RegexBodyAssertion" + }, + { + "$ref": "#/components/schemas/ResponseSizeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/SslExpiryAssertion" + }, + { + "$ref": "#/components/schemas/StatusCodeAssertion" + }, + { + "$ref": "#/components/schemas/TcpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" + } + ] + }, + "severity": { + "type": "string", + "enum": [ + "fail", + "warn" + ] + } + } + }, + "MonitorAssertionDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid" + }, + "assertionType": { + "type": "string", + "enum": [ + "status_code", + "response_time", + "body_contains", + "json_path", + "header", + "regex", + "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": { + "oneOf": [ + { + "$ref": "#/components/schemas/BodyContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedCnameAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedIpsAssertion" + }, + { + "$ref": "#/components/schemas/DnsMaxAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsMinAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordEqualsAssertion" + }, + { + "$ref": "#/components/schemas/DnsResolvesAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlHighAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlLowAssertion" + }, + { + "$ref": "#/components/schemas/DnsTxtContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeaderValueAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatReceivedAssertion" + }, + { + "$ref": "#/components/schemas/IcmpPacketLossAssertion" + }, + { + "$ref": "#/components/schemas/IcmpReachableAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/JsonPathAssertion" + }, + { + "$ref": "#/components/schemas/McpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/McpHasCapabilityAssertion" + }, + { + "$ref": "#/components/schemas/McpMinToolsAssertion" + }, + { + "$ref": "#/components/schemas/McpProtocolVersionAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/McpToolAvailableAssertion" + }, + { + "$ref": "#/components/schemas/McpToolCountChangedAssertion" + }, + { + "$ref": "#/components/schemas/RedirectCountAssertion" + }, + { + "$ref": "#/components/schemas/RedirectTargetAssertion" + }, + { + "$ref": "#/components/schemas/RegexBodyAssertion" + }, + { + "$ref": "#/components/schemas/ResponseSizeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/SslExpiryAssertion" + }, + { + "$ref": "#/components/schemas/StatusCodeAssertion" + }, + { + "$ref": "#/components/schemas/TcpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" + } + ] + }, + "severity": { + "type": "string", + "enum": [ + "fail", + "warn" + ] + } + } + }, + "SingleValueResponseMonitorAssertionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MonitorAssertionDto" + } + } + }, + "SetAlertChannelsRequest": { + "required": [ + "channelIds" + ], + "type": "object", + "properties": { + "channelIds": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + }, + "SingleValueResponseListUUID": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + }, + "AddMonitorTagsRequest": { + "type": "object", + "properties": { + "tagIds": { + "type": "array", + "description": "IDs of existing org tags to attach", + "nullable": true, + "items": { + "type": "string", + "description": "IDs of existing org tags to attach", + "format": "uuid", + "nullable": true + } + }, + "newTags": { + "type": "array", + "description": "New tags to create (if not already present) and attach", + "nullable": true, + "items": { + "$ref": "#/components/schemas/NewTagRequest" + } + } + }, + "description": "Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both." + }, + "CreateAssertionRequest": { + "required": [ + "config" + ], + "type": "object", + "properties": { + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/BodyContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedCnameAssertion" + }, + { + "$ref": "#/components/schemas/DnsExpectedIpsAssertion" + }, + { + "$ref": "#/components/schemas/DnsMaxAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsMinAnswersAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordContainsAssertion" + }, + { + "$ref": "#/components/schemas/DnsRecordEqualsAssertion" + }, + { + "$ref": "#/components/schemas/DnsResolvesAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlHighAssertion" + }, + { + "$ref": "#/components/schemas/DnsTtlLowAssertion" + }, + { + "$ref": "#/components/schemas/DnsTxtContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeaderValueAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" + }, + { + "$ref": "#/components/schemas/HeartbeatReceivedAssertion" + }, + { + "$ref": "#/components/schemas/IcmpPacketLossAssertion" + }, + { + "$ref": "#/components/schemas/IcmpReachableAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/JsonPathAssertion" + }, + { + "$ref": "#/components/schemas/McpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/McpHasCapabilityAssertion" + }, + { + "$ref": "#/components/schemas/McpMinToolsAssertion" + }, + { + "$ref": "#/components/schemas/McpProtocolVersionAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/McpToolAvailableAssertion" + }, + { + "$ref": "#/components/schemas/McpToolCountChangedAssertion" + }, + { + "$ref": "#/components/schemas/RedirectCountAssertion" + }, + { + "$ref": "#/components/schemas/RedirectTargetAssertion" + }, + { + "$ref": "#/components/schemas/RegexBodyAssertion" + }, + { + "$ref": "#/components/schemas/ResponseSizeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/ResponseTimeWarnAssertion" + }, + { + "$ref": "#/components/schemas/SslExpiryAssertion" + }, + { + "$ref": "#/components/schemas/StatusCodeAssertion" + }, + { + "$ref": "#/components/schemas/TcpConnectsAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeAssertion" + }, + { + "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" + } + ] + }, + "severity": { + "type": "string", + "enum": [ + "fail", + "warn" + ] + } + }, + "description": "Replace all assertions; null preserves current" + }, + "DnsMonitorConfig": { + "required": [ + "hostname" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "hostname": { + "minLength": 1, + "type": "string", + "description": "Domain name to resolve" + }, + "recordTypes": { + "type": "array", + "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", + "nullable": true, + "items": { + "type": "string", + "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", + "nullable": true, + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "TXT", + "SRV", + "SOA", + "CAA", + "PTR" + ] + } + }, + "nameservers": { + "type": "array", + "description": "Custom nameservers to query (uses system defaults if omitted)", + "nullable": true, + "items": { + "type": "string", + "description": "Custom nameservers to query (uses system defaults if omitted)", + "nullable": true + } + }, + "timeoutMs": { + "type": "integer", + "description": "Per-query timeout in milliseconds", + "format": "int32", + "nullable": true + }, + "totalTimeoutMs": { + "type": "integer", + "description": "Total timeout for all queries in milliseconds", + "format": "int32", + "nullable": true + } + } + } + ] + }, + "HeartbeatMonitorConfig": { + "required": [ + "expectedInterval", + "gracePeriod" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "expectedInterval": { + "maximum": 86400, + "minimum": 1, + "type": "integer", + "description": "Expected heartbeat interval in seconds", + "format": "int32" + }, + "gracePeriod": { + "minimum": 1, + "type": "integer", + "description": "Grace period in seconds before marking as down", + "format": "int32" + } + } + } + ] + }, + "HttpMonitorConfig": { + "required": [ + "method", + "url" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "url": { + "minLength": 1, + "type": "string", + "description": "Target URL to send requests to" + }, + "method": { + "type": "string", + "description": "HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD" + ] + }, + "customHeaders": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Additional HTTP headers to include in requests", + "nullable": true + }, + "description": "Additional HTTP headers to include in requests", + "nullable": true + }, + "requestBody": { + "type": "string", + "description": "Request body content for POST/PUT/PATCH methods", + "nullable": true + }, + "contentType": { + "type": "string", + "description": "Content-Type header value for the request body", + "nullable": true + }, + "verifyTls": { + "type": "boolean", + "description": "Whether to verify TLS certificates (default: true)", + "nullable": true + } + } + } + ] + }, + "IcmpMonitorConfig": { + "required": [ + "host" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "host": { + "minLength": 1, + "type": "string", + "description": "Target hostname or IP address to ping" + }, + "packetCount": { + "maximum": 20, + "minimum": 1, + "type": "integer", + "description": "Number of ICMP packets to send", + "format": "int32", + "nullable": true + }, + "timeoutMs": { + "type": "integer", + "description": "Ping timeout in milliseconds", + "format": "int32", + "nullable": true + } + } + } + ] + }, + "McpServerMonitorConfig": { + "required": [ + "command" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "command": { + "minLength": 1, + "type": "string", + "description": "Command to execute to start the MCP server" + }, + "args": { + "type": "array", + "description": "Command-line arguments for the MCP server process", + "nullable": true, + "items": { + "type": "string", + "description": "Command-line arguments for the MCP server process", + "nullable": true + } + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Environment variables to pass to the MCP server process", + "nullable": true + }, + "description": "Environment variables to pass to the MCP server process", + "nullable": true + } + } + } + ] + }, + "MonitorConfig": { + "type": "object", + "description": "Updated protocol-specific configuration; null preserves current" + }, + "NewTagRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "minLength": 0, + "type": "string", + "description": "Tag name" + }, + "color": { + "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", + "type": "string", + "description": "Hex color code (defaults to #6B7280 if omitted)", + "nullable": true + } + }, + "description": "Inline tag creation \u2014 creates the tag if it does not already exist" + }, + "TcpMonitorConfig": { + "required": [ + "host" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MonitorConfig" + }, + { + "type": "object", + "properties": { + "host": { + "minLength": 1, + "type": "string", + "description": "Target hostname or IP address" + }, + "port": { + "maximum": 65535, + "minimum": 1, + "type": "integer", + "description": "TCP port to connect to", + "format": "int32" + }, + "timeoutMs": { + "type": "integer", + "description": "Connection timeout in milliseconds", + "format": "int32", + "nullable": true + } + } + } + ] + }, + "UpdateMonitorRequest": { + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "New monitor name; null preserves current", + "nullable": true + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] + }, + "frequencySeconds": { + "type": "integer", + "description": "New check frequency in seconds (30\u201386400); null preserves current", + "format": "int32", + "nullable": true + }, + "enabled": { + "type": "boolean", + "description": "Enable or disable the monitor; null preserves current", + "nullable": true + }, + "regions": { + "type": "array", + "description": "New probe regions; null preserves current", + "nullable": true, + "items": { + "type": "string", + "description": "New probe regions; null preserves current", + "nullable": true + } + }, + "managedBy": { + "type": "string", + "description": "New management source; null preserves current", + "nullable": true, + "enum": [ + "DASHBOARD", + "CLI" + ] + }, + "environmentId": { + "type": "string", + "description": "New environment ID; null preserves current (use clearEnvironmentId to unset)", + "format": "uuid", + "nullable": true + }, + "clearEnvironmentId": { + "type": "boolean", + "description": "Set to true to remove the environment association", + "nullable": true + }, + "assertions": { + "type": "array", + "description": "Replace all assertions; null preserves current", + "nullable": true, + "items": { + "$ref": "#/components/schemas/CreateAssertionRequest" + } + }, + "auth": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + }, + "clearAuth": { + "type": "boolean", + "description": "Set to true to remove authentication", + "nullable": true + }, + "incidentPolicy": { + "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" + }, + "alertChannelIds": { + "type": "array", + "description": "Replace alert channel list; null preserves current", + "nullable": true, + "items": { + "type": "string", + "description": "Replace alert channel list; null preserves current", + "format": "uuid", + "nullable": true + } + }, + "tags": { + "$ref": "#/components/schemas/AddMonitorTagsRequest" + } + } + }, + "MonitorDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] + }, + "frequencySeconds": { + "type": "integer", + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "managedBy": { + "type": "string", + "enum": [ + "DASHBOARD", + "CLI" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "assertions": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/MonitorAssertionDto" + } + }, + "tags": { + "type": "array", + "nullable": true, + "items": { + "$ref": "#/components/schemas/TagDto" + } + }, + "pingUrl": { + "type": "string", + "nullable": true + }, + "environment": { + "$ref": "#/components/schemas/Summary" + }, + "auth": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + }, + "incidentPolicy": { + "$ref": "#/components/schemas/IncidentPolicyDto" + }, + "alertChannelIds": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "uuid", + "nullable": true + } + } + } + }, + "SingleValueResponseMonitorDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MonitorDto" + } + } + }, + "Summary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + } + }, + "ChangeStatusRequest": { + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "INVITED", + "ACTIVE", + "SUSPENDED", + "LEFT", + "REMOVED", + "DECLINED" + ] + } + } + }, + "UpdateMaintenanceWindowRequest": { + "required": [ + "endsAt", + "startsAt" + ], + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "format": "uuid" + }, + "startsAt": { + "type": "string", + "format": "date-time" + }, + "endsAt": { + "type": "string", + "format": "date-time" + }, + "repeatRule": { + "maxLength": 100, + "minLength": 0, + "type": "string" + }, + "reason": { + "type": "string" + }, + "suppressAlerts": { + "type": "boolean" + } + } + }, + "MaintenanceWindowDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "startsAt": { + "type": "string", + "format": "date-time" + }, + "endsAt": { + "type": "string", + "format": "date-time" + }, + "repeatRule": { + "type": "string", + "nullable": true + }, + "reason": { + "type": "string", + "nullable": true + }, + "suppressAlerts": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseMaintenanceWindowDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MaintenanceWindowDto" + } + } + }, + "UpdateEnvironmentRequest": { + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "minLength": 0, + "type": "string", + "description": "New environment name; null preserves current", + "nullable": true + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Replace all variables; null preserves current", + "nullable": true + }, + "description": "Replace all variables; null preserves current", + "nullable": true + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default environment; null preserves current", + "nullable": true + } + } + }, + "EnvironmentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "orgId": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "monitorCount": { + "type": "integer", + "format": "int32" + }, + "isDefault": { + "type": "boolean" + } + } + }, + "SingleValueResponseEnvironmentDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/EnvironmentDto" + } + } + }, + "ChannelConfig": { + "required": [ + "channelType" + ], + "type": "object", + "properties": { + "channelType": { + "type": "string" + } + }, + "description": "New channel configuration (full replacement, not partial update)", + "discriminator": { + "propertyName": "channelType" + } + }, + "DiscordChannelConfig": { + "required": [ + "webhookUrl" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Discord webhook URL" + }, + "mentionRoleId": { + "type": "string", + "description": "Optional Discord role ID to mention in notifications", + "nullable": true + } + } + } + ] + }, + "EmailChannelConfig": { + "required": [ + "recipients" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "recipients": { + "minItems": 1, + "type": "array", + "description": "Email addresses to send notifications to", + "items": { + "type": "string", + "description": "Email addresses to send notifications to", + "format": "email" + } + } + } + } + ] + }, + "OpsGenieChannelConfig": { + "required": [ + "apiKey" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "apiKey": { + "minLength": 1, + "type": "string", + "description": "OpsGenie API key for alert creation" + }, + "region": { + "type": "string", + "description": "OpsGenie API region: us or eu", + "nullable": true + } + } + } + ] + }, + "PagerDutyChannelConfig": { + "required": [ + "routingKey" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "routingKey": { + "minLength": 1, + "type": "string", + "description": "PagerDuty Events API v2 routing (integration) key" + }, + "severityOverride": { + "type": "string", + "description": "Override PagerDuty severity mapping", + "nullable": true + } + } + } + ] + }, + "SlackChannelConfig": { + "required": [ + "webhookUrl" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Slack incoming webhook URL" + }, + "mentionText": { + "type": "string", + "description": "Optional mention text included in notifications, e.g. @channel", + "nullable": true + } + } + } + ] + }, + "TeamsChannelConfig": { + "required": [ + "webhookUrl" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "webhookUrl": { + "minLength": 1, + "type": "string", + "description": "Microsoft Teams incoming webhook URL" + } + } + } + ] + }, + "UpdateAlertChannelRequest": { + "required": [ + "config", + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "New channel name (full replacement, not partial update)" + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DiscordChannelConfig" + }, + { + "$ref": "#/components/schemas/EmailChannelConfig" + }, + { + "$ref": "#/components/schemas/OpsGenieChannelConfig" + }, + { + "$ref": "#/components/schemas/PagerDutyChannelConfig" + }, + { + "$ref": "#/components/schemas/SlackChannelConfig" + }, + { + "$ref": "#/components/schemas/TeamsChannelConfig" + }, + { + "$ref": "#/components/schemas/WebhookChannelConfig" + } + ] + } + } + }, + "WebhookChannelConfig": { + "required": [ + "url" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ChannelConfig" + }, + { + "type": "object", + "properties": { + "url": { + "minLength": 1, + "type": "string", + "description": "Webhook endpoint URL that receives alert payloads" + }, + "signingSecret": { + "type": "string", + "description": "Optional HMAC signing secret for payload verification", + "nullable": true + }, + "customHeaders": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Additional HTTP headers to include in webhook requests", + "nullable": true + }, + "description": "Additional HTTP headers to include in webhook requests", + "nullable": true + } + } + } + ] + }, + "AlertChannelDto": { + "required": [ + "channelType", + "createdAt", + "id", + "name", + "updatedAt" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "channelType": { + "type": "string", + "enum": [ + "email", + "webhook", + "slack", + "pagerduty", + "opsgenie", + "teams", + "discord" + ] + }, + "displayConfig": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + }, + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "configHash": { + "type": "string", + "nullable": true + }, + "lastDeliveryAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastDeliveryStatus": { + "type": "string", + "nullable": true + } + } + }, + "SingleValueResponseAlertChannelDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AlertChannelDto" + } + } + }, + "WorkspaceCreateParams": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "organizationId": { + "type": "integer", + "format": "int32" + }, + "name": { + "minLength": 1, + "type": "string" + } + } + }, + "ServiceIncidentRequest": { + "required": [ + "action", + "externalRef", + "serviceId", + "title" + ], + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "format": "uuid" + }, + "externalRef": { + "minLength": 1, + "type": "string" + }, + "severity": { + "type": "string", + "nullable": true + }, + "title": { + "minLength": 1, + "type": "string" + }, + "shortlink": { + "type": "string", + "nullable": true + }, + "affectedComponents": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "serviceIncidentId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "action": { + "minLength": 1, + "type": "string" + }, + "statusText": { + "type": "string", + "nullable": true + } + } + }, + "IncidentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "organizationId": { + "type": "integer", + "format": "int32" + }, + "source": { + "type": "string", + "enum": [ + "AUTOMATIC", + "MANUAL", + "MONITORS", + "STATUS_DATA", + "RESOURCE_GROUP" + ] + }, + "status": { + "type": "string", + "enum": [ + "WATCHING", + "TRIGGERED", + "CONFIRMED", + "RESOLVED" + ] + }, + "severity": { + "type": "string", + "enum": [ + "DOWN", + "DEGRADED", + "MAINTENANCE" + ] + }, + "title": { + "type": "string", + "nullable": true + }, + "triggeredByRule": { + "type": "string", + "nullable": true + }, + "affectedRegions": { + "type": "array", + "items": { + "type": "string" + } + }, + "reopenCount": { + "type": "integer", + "format": "int32" + }, + "createdByUserId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "statusPageVisible": { + "type": "boolean" + }, + "serviceIncidentId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "serviceId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "externalRef": { + "type": "string", + "nullable": true + }, + "affectedComponents": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "shortlink": { + "type": "string", + "nullable": true + }, + "resolutionReason": { + "type": "string", + "nullable": true, + "enum": [ + "MANUAL", + "AUTO_RECOVERED", + "AUTO_RESOLVED" + ] + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "confirmedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resolvedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "cooldownUntil": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "monitorName": { + "type": "string", + "nullable": true + }, + "serviceName": { + "type": "string", + "nullable": true + }, + "serviceSlug": { + "type": "string", + "nullable": true + }, + "monitorType": { + "type": "string", + "nullable": true + }, + "resourceGroupId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "resourceGroupName": { + "type": "string", + "nullable": true + } + } + }, + "TableValueResultIncidentDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncidentDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseInteger": { + "type": "object", + "properties": { + "data": { + "type": "integer", + "format": "int32" + } + } + }, + "CreateAutoIncidentRequest": { + "required": [ + "monitorId" + ], + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "format": "uuid" + }, + "severity": { + "type": "string", + "nullable": true + }, + "triggeredByRule": { + "type": "string", + "nullable": true + }, + "affectedRegions": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseIncidentDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/IncidentDto" + } + } + }, + "ReopenAutoIncidentRequest": { + "type": "object", + "properties": { + "affectedRegions": { + "type": "array", + "items": { + "type": "string" + } + }, + "severity": { + "type": "string", + "nullable": true + } + } + }, + "AdapterHealthReportRequest": { + "required": [ + "serviceId", + "success" + ], + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "format": "uuid" + }, + "success": { + "type": "boolean" + }, + "errorMessage": { + "type": "string", + "nullable": true + } + } + }, + "AdapterHealthDto": { + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "format": "uuid" + }, + "serviceSlug": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "adapterType": { + "type": "string", + "nullable": true + }, + "lastSuccessAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastFailureAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "consecutiveFailures": { + "type": "integer", + "format": "int32" + }, + "lastErrorMessage": { + "type": "string", + "nullable": true + }, + "disabledByHealth": { + "type": "boolean" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseAdapterHealthDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AdapterHealthDto" + } + } + }, + "CreateOrgRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "nullable": true + } + } + }, + "SingleValueResponseTransactionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TransactionDto" + } + } + }, + "TransactionDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "nullable": true + }, + "currencyCode": { + "type": "string", + "nullable": true + }, + "invoiceNumber": { + "type": "string", + "nullable": true + }, + "billedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "total": { + "type": "string", + "nullable": true + }, + "subtotal": { + "type": "string", + "nullable": true + }, + "tax": { + "type": "string", + "nullable": true + } + } + }, + "QuickMonitorRequest": { + "required": [ + "url" + ], + "type": "object", + "properties": { + "url": { + "minLength": 1, + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "frequencySeconds": { + "type": "integer", + "format": "int32", + "nullable": true + } + } + }, + "OnboardingSetupRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string" + }, + "role": { + "maxLength": 50, + "minLength": 0, + "type": "string", + "nullable": true + }, + "teamSize": { + "maxLength": 50, + "minLength": 0, + "type": "string", + "nullable": true + } + } + }, + "AnalyzeUrlRequest": { + "required": [ + "url" + ], + "type": "object", + "properties": { + "url": { + "minLength": 1, + "type": "string" + } + } + }, + "AnalyzeUrlResponse": { + "type": "object", + "properties": { + "reachable": { + "type": "boolean" + }, + "responseTimeMs": { + "type": "integer", + "format": "int64" + }, + "statusCode": { + "type": "integer", + "format": "int32" + }, + "tlsExpiry": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "tlsDaysRemaining": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "contentType": { + "type": "string", + "nullable": true + }, + "suggestedName": { + "type": "string" + }, + "suggestedAssertions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SuggestedAssertion" + } + }, + "suggestedFrequencySeconds": { + "type": "integer", + "format": "int32" + } + } + }, + "SingleValueResponseAnalyzeUrlResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AnalyzeUrlResponse" + } + } + }, + "SuggestedAssertion": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, + "AcceptInviteRequest": { + "required": [ + "token" + ], + "type": "object", + "properties": { + "token": { + "minLength": 1, + "type": "string" + } + } + }, + "AcceptInviteDto": { + "type": "object", + "properties": { + "orgId": { + "type": "integer", + "format": "int32" + }, + "userId": { + "type": "integer", + "format": "int32" + }, + "orgRole": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + }, + "status": { + "type": "string", + "enum": [ + "INVITED", + "ACTIVE", + "SUSPENDED", + "LEFT", + "REMOVED", + "DECLINED" + ] + } + } + }, + "SingleValueResponseAcceptInviteDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AcceptInviteDto" + } + } + }, + "RegisterUserRequest": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "picture": { + "type": "string", + "nullable": true + } + } + }, + "CreateWorkspaceRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + } + } + }, + "AddMemberRequest": { + "required": [ + "orgRole", + "userId" + ], + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int32" + }, + "orgRole": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + } + } + }, + "MemberDto": { + "type": "object", + "properties": { + "userId": { + "type": "integer", + "format": "int32" + }, + "email": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "orgRole": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + }, + "status": { + "type": "string", + "enum": [ + "INVITED", + "ACTIVE", + "SUSPENDED", + "LEFT", + "REMOVED", + "DECLINED" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseMemberDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MemberDto" + } + } + }, + "CreateWebhookEndpointRequest": { + "required": [ + "subscribedEvents", + "url" + ], + "type": "object", + "properties": { + "url": { + "maxLength": 2048, + "minLength": 0, + "type": "string", + "description": "HTTPS endpoint that receives webhook event payloads" + }, + "description": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Optional human-readable description" + }, + "subscribedEvents": { + "minItems": 1, + "type": "array", + "description": "Event types to deliver, e.g. monitor.created, incident.resolved", + "items": { + "minLength": 1, + "type": "string", + "description": "Event types to deliver, e.g. monitor.created, incident.resolved" + } + } + } + }, + "TestWebhookEndpointRequest": { + "type": "object", + "properties": { + "eventType": { + "type": "string", + "nullable": true + } + } + }, + "SingleValueResponseWebhookTestResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/WebhookTestResult" + } + } + }, + "WebhookTestResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "statusCode": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "message": { + "type": "string" + }, + "durationMs": { + "type": "integer", + "format": "int64", + "nullable": true + } + } + }, + "SingleValueResponseString": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + }, + "DekRotationResultDto": { + "type": "object", + "properties": { + "previousDekVersion": { + "type": "integer", + "format": "int32" + }, + "newDekVersion": { + "type": "integer", + "format": "int32" + }, + "secretsReEncrypted": { + "type": "integer", + "format": "int32" + }, + "channelsReEncrypted": { + "type": "integer", + "format": "int32" + }, + "rotatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseDekRotationResultDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/DekRotationResultDto" + } + } + }, + "CreateTagRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "minLength": 0, + "type": "string", + "description": "Tag name, unique within the org" + }, + "color": { + "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", + "type": "string", + "description": "Hex color code (defaults to #6B7280 if omitted)", + "nullable": true + } + }, + "description": "Request body for creating a tag" + }, + "ServiceSubscribeRequest": { + "type": "object", + "properties": { + "componentId": { + "type": "string", + "description": "ID of the component to subscribe to. Omit or null for whole-service subscription.", + "format": "uuid", + "nullable": true + }, + "alertSensitivity": { + "type": "string", + "description": "Alert sensitivity level. Defaults to INCIDENTS_ONLY when not provided.", + "nullable": true + } + }, + "description": "Optional body for subscribing to a specific component of a service" + }, + "ComponentUptimeSummaryDto": { + "type": "object", + "properties": { + "day": { + "type": "number", + "description": "Uptime percentage over the last 24 hours", + "format": "double", + "nullable": true, + "example": 99.95 + }, + "week": { + "type": "number", + "description": "Uptime percentage over the last 7 days", + "format": "double", + "nullable": true, + "example": 99.98 + }, + "month": { + "type": "number", + "description": "Uptime percentage over the last 30 days", + "format": "double", + "nullable": true, + "example": 99.92 + }, + "source": { + "type": "string", + "description": "Data source: vendor_reported or incident_derived", + "example": "vendor_reported" + } + }, + "description": "Inline uptime percentages for 24h, 7d, 30d" + }, + "ServiceComponentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "externalId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "groupId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "position": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "showcase": { + "type": "boolean" + }, + "onlyShowIfDegraded": { + "type": "boolean" + }, + "startDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "vendorCreatedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lifecycleStatus": { + "type": "string" + }, + "dataType": { + "type": "string", + "description": "Data classification: full, status_only, or metric_only", + "example": "full" + }, + "hasUptime": { + "type": "boolean", + "description": "Whether uptime data is available for this component" + }, + "region": { + "type": "string", + "description": "Geographic region for regional components (AWS, GCP, Azure)", + "nullable": true + }, + "groupName": { + "type": "string", + "description": "Display name of the parent group", + "nullable": true + }, + "uptime": { + "$ref": "#/components/schemas/ComponentUptimeSummaryDto" + }, + "statusChangedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "firstSeenAt": { + "type": "string", + "format": "date-time" + }, + "lastSeenAt": { + "type": "string", + "format": "date-time" + }, + "group": { + "type": "boolean" + } + }, + "description": "A first-class service component with lifecycle and uptime data" + }, + "ServiceSubscriptionDto": { + "type": "object", + "properties": { + "subscriptionId": { + "type": "string", + "description": "Unique subscription identifier", + "format": "uuid" + }, + "serviceId": { + "type": "string", + "description": "Service identifier", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string", + "nullable": true + }, + "officialStatusUrl": { + "type": "string", + "nullable": true + }, + "adapterType": { + "type": "string" + }, + "pollingIntervalSeconds": { + "type": "integer", + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "logoUrl": { + "type": "string", + "description": "Logo URL from the service catalog", + "nullable": true + }, + "overallStatus": { + "type": "string", + "description": "Current overall status; null when the service has never been polled", + "nullable": true + }, + "componentId": { + "type": "string", + "description": "Subscribed component id; null for whole-service subscription", + "format": "uuid", + "nullable": true + }, + "component": { + "$ref": "#/components/schemas/ServiceComponentDto" + }, + "alertSensitivity": { + "type": "string", + "description": "Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity)", + "enum": [ + "ALL", + "INCIDENTS_ONLY", + "MAJOR_ONLY" + ] + }, + "subscribedAt": { + "type": "string", + "description": "When the organization subscribed to this service", + "format": "date-time" + } + }, + "description": "An org-level service subscription with current status information" + }, + "SingleValueResponseServiceSubscriptionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ServiceSubscriptionDto" + } + } + }, + "CreateSecretRequest": { + "required": [ + "key", + "value" + ], + "type": "object", + "properties": { + "key": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Unique secret key within the workspace (max 255 chars)" + }, + "value": { + "maxLength": 32768, + "minLength": 0, + "type": "string", + "description": "Secret value, stored encrypted (max 32KB)" + } + } + }, + "CreateResourceGroupRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this group" + }, + "description": { + "type": "string", + "description": "Optional description", + "nullable": true + }, + "alertPolicyId": { + "type": "string", + "description": "Optional notification policy to apply for this group", + "format": "uuid", + "nullable": true + }, + "defaultFrequency": { + "maximum": 86400, + "minimum": 30, + "type": "integer", + "description": "Default check frequency in seconds applied to members (30\u201386400)", + "format": "int32", + "nullable": true + }, + "defaultRegions": { + "type": "array", + "description": "Default regions applied to member monitors", + "nullable": true, + "items": { + "type": "string", + "description": "Default regions applied to member monitors", + "nullable": true + } + }, + "defaultRetryStrategy": { + "$ref": "#/components/schemas/RetryStrategy" + }, + "defaultAlertChannels": { + "type": "array", + "description": "Default alert channel IDs applied to member monitors", + "nullable": true, + "items": { + "type": "string", + "description": "Default alert channel IDs applied to member monitors", + "format": "uuid", + "nullable": true + } + }, + "defaultEnvironmentId": { + "type": "string", + "description": "Default environment ID applied to member monitors", + "format": "uuid", + "nullable": true + }, + "healthThresholdType": { + "type": "string", + "description": "Health threshold type: COUNT or PERCENTAGE", + "nullable": true, + "enum": [ + "COUNT", + "PERCENTAGE" + ] + }, + "healthThresholdValue": { + "maximum": 100, + "exclusiveMaximum": false, + "minimum": 0, + "exclusiveMinimum": false, + "type": "number", + "description": "Health threshold value: count (0+) or percentage (0\u2013100)", + "nullable": true + }, + "suppressMemberAlerts": { + "type": "boolean", + "description": "Suppress member-level alert notifications when group manages alerting", + "nullable": true + }, + "confirmationDelaySeconds": { + "maximum": 600, + "minimum": 0, + "type": "integer", + "description": "Confirmation delay in seconds before group incident creation (0\u2013600)", + "format": "int32", + "nullable": true + }, + "recoveryCooldownMinutes": { + "maximum": 60, + "minimum": 0, + "type": "integer", + "description": "Recovery cooldown in minutes after group incident resolves (0\u201360)", + "format": "int32", + "nullable": true + } + }, + "description": "Request body for creating a resource group" + }, + "AddResourceGroupMemberRequest": { + "required": [ + "memberId", + "memberType" + ], + "type": "object", + "properties": { + "memberType": { + "minLength": 1, + "pattern": "monitor|service", + "type": "string", + "description": "Type of member: 'monitor' or 'service'" + }, + "memberId": { + "type": "string", + "description": "ID of the monitor or service to add", + "format": "uuid" + } + }, + "description": "Request body for adding a member to a resource group" + }, + "SingleValueResponseResourceGroupMemberDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ResourceGroupMemberDto" + } + } + }, + "CreateNotificationPolicyRequest": { + "required": [ + "escalation", + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this policy" + }, + "matchRules": { + "type": "array", + "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)", + "items": { + "$ref": "#/components/schemas/MatchRule" + } + }, + "escalation": { + "$ref": "#/components/schemas/EscalationChain" + }, + "enabled": { + "type": "boolean", + "description": "Whether this policy is enabled (default true)", + "default": true + }, + "priority": { + "type": "integer", + "description": "Evaluation priority; higher value = evaluated first (default 0)", + "format": "int32", + "default": 0 + } + }, + "description": "Request body for creating a notification policy" + }, + "TestNotificationPolicyRequest": { + "type": "object", + "properties": { + "severity": { + "type": "string", + "description": "Incident severity to test against (e.g. DOWN, DEGRADED, MAINTENANCE)", + "nullable": true + }, + "monitorId": { + "type": "string", + "description": "Monitor UUID to test against (monitoring events)", + "format": "uuid", + "nullable": true + }, + "regions": { + "type": "array", + "description": "Affected region identifiers to test against (monitoring events)", + "nullable": true, + "items": { + "type": "string", + "description": "Affected region identifiers to test against (monitoring events)", + "nullable": true + } + }, + "eventType": { + "type": "string", + "description": "Incident event type to test against \u2014 short form (e.g. created, resolved, reopened) or full form (e.g. incident.created)", + "nullable": true + }, + "monitorType": { + "type": "string", + "description": "Monitor check type to test against (e.g. HTTP, DNS, MCP_SERVER)", + "nullable": true + }, + "serviceId": { + "type": "string", + "description": "Service catalog UUID to test against (status data events)", + "format": "uuid", + "nullable": true + }, + "componentName": { + "type": "string", + "description": "Component name to test against (status data events, e.g. \"Actions\")", + "nullable": true + }, + "resourceGroupIds": { + "type": "array", + "description": "Resource group UUIDs the entity belongs to, for resource_group_id_in rules", + "nullable": true, + "items": { + "type": "string", + "description": "Resource group UUIDs the entity belongs to, for resource_group_id_in rules", + "format": "uuid", + "nullable": true + } + } + }, + "description": "Event context for a dry-run match evaluation against a notification policy" + }, + "SingleValueResponseTestMatchResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TestMatchResult" + } + } + }, + "TestMatchResult": { + "type": "object", + "properties": { + "matched": { + "type": "boolean", + "description": "Whether the policy would match the supplied incident context" + }, + "matchedRules": { + "type": "array", + "description": "Rules that passed evaluation", + "items": { + "type": "string", + "description": "Rules that passed evaluation" + } + }, + "unmatchedRules": { + "type": "array", + "description": "Rules that did not pass evaluation", + "items": { + "type": "string", + "description": "Rules that did not pass evaluation" + } + } + }, + "description": "Result of a dry-run match evaluation against a notification policy" + }, + "AlertDeliveryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "incidentId": { + "type": "string", + "description": "Incident that triggered this delivery", + "format": "uuid" + }, + "dispatchId": { + "type": "string", + "description": "Notification dispatch that created this delivery", + "format": "uuid", + "nullable": true + }, + "channelId": { + "type": "string", + "description": "Alert channel ID", + "format": "uuid" + }, + "channel": { + "type": "string", + "description": "Human-readable channel name" + }, + "channelType": { + "type": "string", + "description": "Alert channel type (e.g. slack, email, webhook)" + }, + "status": { + "type": "string", + "description": "Current delivery status", + "enum": [ + "PENDING", + "DELIVERED", + "RETRY_PENDING", + "FAILED", + "CANCELLED" + ] + }, + "eventType": { + "type": "string", + "description": "Incident lifecycle event that triggered this delivery", + "enum": [ + "INCIDENT_CREATED", + "INCIDENT_RESOLVED", + "INCIDENT_REOPENED" + ] + }, + "stepNumber": { + "type": "integer", + "description": "1-based escalation step this delivery belongs to", + "format": "int32" + }, + "fireCount": { + "type": "integer", + "description": "Fire sequence within the step: 1 = initial, 2+ = repeat re-fires", + "format": "int32" + }, + "attemptCount": { + "type": "integer", + "description": "Number of delivery attempts made", + "format": "int32" + }, + "lastAttemptAt": { + "type": "string", + "description": "When the last attempt was made", + "format": "date-time", + "nullable": true + }, + "nextRetryAt": { + "type": "string", + "description": "When the next retry is scheduled (null if not retrying)", + "format": "date-time", + "nullable": true + }, + "deliveredAt": { + "type": "string", + "description": "Timestamp when the delivery was confirmed (null if not yet delivered)", + "format": "date-time", + "nullable": true + }, + "errorMessage": { + "type": "string", + "description": "Error message from the last failed attempt", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Delivery record for a single channel within a notification dispatch" + }, + "NotificationDispatchDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "incidentId": { + "type": "string", + "format": "uuid" + }, + "policyId": { + "type": "string", + "format": "uuid" + }, + "policyName": { + "type": "string", + "description": "Human-readable name of the matched policy (null if policy has been deleted)", + "nullable": true + }, + "status": { + "type": "string", + "description": "Current dispatch state", + "enum": [ + "PENDING", + "DISPATCHING", + "DELIVERED", + "ESCALATING", + "ACKNOWLEDGED", + "COMPLETED" + ] + }, + "completionReason": { + "type": "string", + "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.", + "nullable": true, + "enum": [ + "EXHAUSTED", + "RESOLVED", + "NO_STEPS" + ] + }, + "currentStep": { + "type": "integer", + "description": "1-based index of the currently active escalation step", + "format": "int32" + }, + "totalSteps": { + "type": "integer", + "description": "Total number of escalation steps in the policy (null if policy has been deleted)", + "format": "int32", + "nullable": true + }, + "acknowledgedAt": { + "type": "string", + "description": "Timestamp when this dispatch was acknowledged (null if not acknowledged)", + "format": "date-time", + "nullable": true + }, + "nextEscalationAt": { + "type": "string", + "description": "Timestamp when the next escalation step will fire (null if not scheduled)", + "format": "date-time", + "nullable": true + }, + "lastNotifiedAt": { + "type": "string", + "description": "Timestamp of the most recent notification delivery", + "format": "date-time", + "nullable": true + }, + "deliveries": { + "type": "array", + "description": "Delivery records for all channels associated with this dispatch", + "items": { + "$ref": "#/components/schemas/AlertDeliveryDto" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Dispatch state for a single (incident, notification policy) pair, with delivery history" + }, + "SingleValueResponseNotificationDispatchDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/NotificationDispatchDto" + } + } + }, + "CreateMonitorRequest": { + "required": [ + "config", + "managedBy", + "name", + "type" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this monitor" + }, + "type": { + "type": "string", + "description": "Monitor protocol type", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] + }, + "frequencySeconds": { + "type": "integer", + "description": "Check frequency in seconds (30\u201386400, default: 60)", + "format": "int32" + }, + "enabled": { + "type": "boolean", + "description": "Whether the monitor is active (default: true)", + "nullable": true + }, + "regions": { + "type": "array", + "description": "Probe regions to run checks from, e.g. us-east, eu-west", + "nullable": true, + "items": { + "type": "string", + "description": "Probe regions to run checks from, e.g. us-east, eu-west", + "nullable": true + } + }, + "managedBy": { + "type": "string", + "description": "Who manages this monitor: DASHBOARD or CLI", + "enum": [ + "DASHBOARD", + "CLI" + ] + }, + "environmentId": { + "type": "string", + "description": "Environment to associate with this monitor", + "format": "uuid", + "nullable": true + }, + "assertions": { + "type": "array", + "description": "Assertions to evaluate against each check result", + "nullable": true, + "items": { + "$ref": "#/components/schemas/CreateAssertionRequest" + } + }, + "auth": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + }, + "incidentPolicy": { + "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" + }, + "alertChannelIds": { + "type": "array", + "description": "Alert channels to notify when this monitor triggers", + "nullable": true, + "items": { + "type": "string", + "description": "Alert channels to notify when this monitor triggers", + "format": "uuid", + "nullable": true + } + }, + "tags": { + "$ref": "#/components/schemas/AddMonitorTagsRequest" + } + } + }, + "SetMonitorAuthRequest": { + "required": [ + "config" + ], + "type": "object", + "properties": { + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApiKeyAuthConfig" + }, + { + "$ref": "#/components/schemas/BasicAuthConfig" + }, + { + "$ref": "#/components/schemas/BearerAuthConfig" + }, + { + "$ref": "#/components/schemas/HeaderAuthConfig" + } + ] + } + } + }, + "AssertionTestResultDto": { + "type": "object", + "properties": { + "assertionType": { + "type": "string", + "enum": [ + "status_code", + "response_time", + "body_contains", + "json_path", + "header", + "regex", + "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" + ] + }, + "passed": { + "type": "boolean" + }, + "severity": { + "type": "string", + "enum": [ + "fail", + "warn" + ] + }, + "message": { + "type": "string" + }, + "expected": { + "type": "string", + "nullable": true + }, + "actual": { + "type": "string", + "nullable": true + } + } + }, + "MonitorTestResultDto": { + "type": "object", + "properties": { + "passed": { + "type": "boolean" + }, + "error": { + "type": "string", + "nullable": true + }, + "statusCode": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "responseTimeMs": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "responseHeaders": { + "type": "object", + "additionalProperties": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "nullable": true + }, + "bodyPreview": { + "type": "string", + "nullable": true + }, + "responseSizeBytes": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "redirectCount": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "finalUrl": { + "type": "string", + "nullable": true + }, + "assertionResults": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AssertionTestResultDto" + } + }, + "warnings": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + } + } + }, + "SingleValueResponseMonitorTestResultDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MonitorTestResultDto" + } + } + }, + "TableValueResultTagDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TagDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "MonitorTestRequest": { + "required": [ + "config", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Monitor protocol type to test", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] + }, + "assertions": { + "type": "array", + "description": "Optional assertions to evaluate against the test result", + "nullable": true, + "items": { + "$ref": "#/components/schemas/CreateAssertionRequest" + } + } + } + }, + "BulkMonitorActionRequest": { + "required": [ + "action", + "monitorIds" + ], + "type": "object", + "properties": { + "monitorIds": { + "maxItems": 200, + "minItems": 0, + "type": "array", + "description": "IDs of monitors to act on (max 200)", + "items": { + "type": "string", + "description": "IDs of monitors to act on (max 200)", + "format": "uuid" + } + }, + "action": { + "type": "string", + "description": "Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG", + "enum": [ + "PAUSE", + "RESUME", + "DELETE", + "ADD_TAG", + "REMOVE_TAG" + ] + }, + "tagIds": { + "type": "array", + "description": "Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)", + "nullable": true, + "items": { + "type": "string", + "description": "Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)", + "format": "uuid", + "nullable": true + } + }, + "newTags": { + "type": "array", + "description": "New tags to create and attach (only for ADD_TAG)", + "nullable": true, + "items": { + "$ref": "#/components/schemas/NewTagRequest" + } + } + }, + "description": "Request body for performing a bulk action on multiple monitors" + }, + "BulkMonitorActionResult": { + "type": "object", + "properties": { + "succeeded": { + "type": "array", + "description": "IDs of monitors on which the action succeeded", + "items": { + "type": "string", + "description": "IDs of monitors on which the action succeeded", + "format": "uuid" + } + }, + "failed": { + "type": "array", + "description": "Monitors on which the action failed, with the reason for each failure", + "items": { + "$ref": "#/components/schemas/FailureDetail" + } + } + }, + "description": "Result of a bulk monitor action, including partial-success details" + }, + "FailureDetail": { + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "description": "Monitor ID that failed", + "format": "uuid" + }, + "reason": { + "type": "string", + "description": "Human-readable reason for the failure" + } + }, + "description": "Details about a single monitor that failed the bulk action" + }, + "SingleValueResponseBulkMonitorActionResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/BulkMonitorActionResult" + } + } + }, + "CreateMaintenanceWindowRequest": { + "required": [ + "endsAt", + "startsAt" + ], + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "format": "uuid" + }, + "startsAt": { + "type": "string", + "format": "date-time" + }, + "endsAt": { + "type": "string", + "format": "date-time" + }, + "repeatRule": { + "maxLength": 100, + "minLength": 0, + "type": "string" + }, + "reason": { + "type": "string" + }, + "suppressAlerts": { + "type": "boolean" + } + } + }, + "CreateInviteRequest": { + "required": [ + "email", + "roleOffered" + ], + "type": "object", + "properties": { + "email": { + "minLength": 1, + "type": "string", + "format": "email" + }, + "roleOffered": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + } + } + }, + "InviteDto": { + "type": "object", + "properties": { + "inviteId": { + "type": "integer", + "format": "int32" + }, + "email": { + "type": "string" + }, + "roleOffered": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "consumedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "revokedAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseInviteDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/InviteDto" + } + } + }, + "CreateManualIncidentRequest": { + "required": [ + "severity", + "title" + ], + "type": "object", + "properties": { + "title": { + "minLength": 1, + "type": "string", + "description": "Short summary of the incident" + }, + "severity": { + "type": "string", + "description": "Incident severity: DOWN, DEGRADED, or MAINTENANCE", + "enum": [ + "DOWN", + "DEGRADED", + "MAINTENANCE" + ] + }, + "monitorId": { + "type": "string", + "description": "Monitor to associate with this incident", + "format": "uuid", + "nullable": true + }, + "body": { + "type": "string", + "description": "Detailed description or context for the incident", + "nullable": true + } + } + }, + "IncidentDetailDto": { + "type": "object", + "properties": { + "incident": { + "$ref": "#/components/schemas/IncidentDto" + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncidentUpdateDto" + } + } + } + }, + "IncidentUpdateDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "incidentId": { + "type": "string", + "format": "uuid" + }, + "oldStatus": { + "type": "string", + "nullable": true, + "enum": [ + "WATCHING", + "TRIGGERED", + "CONFIRMED", + "RESOLVED" + ] + }, + "newStatus": { + "type": "string", + "nullable": true, + "enum": [ + "WATCHING", + "TRIGGERED", + "CONFIRMED", + "RESOLVED" + ] + }, + "body": { + "type": "string", + "nullable": true + }, + "createdBy": { + "type": "string", + "enum": [ + "SYSTEM", + "USER" + ] + }, + "notifySubscribers": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseIncidentDetailDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/IncidentDetailDto" + } + } + }, + "AddIncidentUpdateRequest": { + "type": "object", + "properties": { + "body": { + "type": "string" + }, + "newStatus": { + "type": "string", + "enum": [ + "WATCHING", + "TRIGGERED", + "CONFIRMED", + "RESOLVED" + ] + }, + "notifySubscribers": { + "type": "boolean" + } + } + }, + "ResolveIncidentRequest": { + "type": "object", + "properties": { + "body": { + "type": "string", + "description": "Optional resolution message or post-mortem notes" + } + } + }, + "CreateEnvironmentRequest": { + "required": [ + "name", + "slug" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "minLength": 0, + "type": "string", + "description": "Human-readable environment name" + }, + "slug": { + "maxLength": 100, + "minLength": 0, + "pattern": "^[a-z0-9][a-z0-9_-]*$", + "type": "string", + "description": "URL-safe identifier (lowercase alphanumeric, hyphens, underscores)" + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Initial key-value variable pairs for this environment", + "nullable": true + }, + "description": "Initial key-value variable pairs for this environment", + "nullable": true + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default environment for new monitors" + } + } + }, + "AcquireDeployLockRequest": { + "required": [ + "lockedBy" + ], + "type": "object", + "properties": { + "lockedBy": { + "minLength": 1, + "type": "string", + "description": "Identity of the lock requester (e.g. hostname, CI job ID)" + }, + "ttlMinutes": { + "type": "integer", + "description": "Lock TTL in minutes. Defaults to 10. Max 60.", + "format": "int32", + "nullable": true, + "example": 10 + } + }, + "description": "Request to acquire a deploy lock for the current workspace" + }, + "DeployLockDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique lock identifier", + "format": "uuid" + }, + "lockedBy": { + "type": "string", + "description": "Identity of the lock holder (e.g. CLI session ID, username)" + }, + "lockedAt": { + "type": "string", + "description": "Timestamp when the lock was acquired", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "description": "Timestamp when the lock automatically expires", + "format": "date-time" + } + }, + "description": "Represents an active deploy lock for a workspace" + }, + "SingleValueResponseDeployLockDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/DeployLockDto" + } + } + }, + "CreateApiKeyRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string", + "description": "Human-readable name to identify this API key" + }, + "expiresAt": { + "type": "string", + "description": "Optional expiration timestamp in ISO 8601 format", + "format": "date-time", + "nullable": true + } + } + }, + "ApiKeyCreateResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "key": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseApiKeyCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiKeyCreateResponse" + } + } + }, + "ApiKeyDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "key": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastUsedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "revokedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseApiKeyDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ApiKeyDto" + } + } + }, + "SingleValueResponseAlertDeliveryDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AlertDeliveryDto" + } + } + }, + "CreateAlertChannelRequest": { + "required": [ + "config", + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "minLength": 0, + "type": "string", + "description": "Human-readable name for this alert channel" + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DiscordChannelConfig" + }, + { + "$ref": "#/components/schemas/EmailChannelConfig" + }, + { + "$ref": "#/components/schemas/OpsGenieChannelConfig" + }, + { + "$ref": "#/components/schemas/PagerDutyChannelConfig" + }, + { + "$ref": "#/components/schemas/SlackChannelConfig" + }, + { + "$ref": "#/components/schemas/TeamsChannelConfig" + }, + { + "$ref": "#/components/schemas/WebhookChannelConfig" + } + ] + } + } + }, + "SingleValueResponseTestChannelResult": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TestChannelResult" + } + } + }, + "TestChannelResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, + "TestAlertChannelRequest": { + "required": [ + "config" + ], + "type": "object", + "properties": { + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DiscordChannelConfig" + }, + { + "$ref": "#/components/schemas/EmailChannelConfig" + }, + { + "$ref": "#/components/schemas/OpsGenieChannelConfig" + }, + { + "$ref": "#/components/schemas/PagerDutyChannelConfig" + }, + { + "$ref": "#/components/schemas/SlackChannelConfig" + }, + { + "$ref": "#/components/schemas/TeamsChannelConfig" + }, + { + "$ref": "#/components/schemas/WebhookChannelConfig" + } + ] + } + } + }, + "ComponentUpdateRequest": { + "required": [ + "addComponents" + ], + "type": "object", + "properties": { + "addComponents": { + "minItems": 1, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UpdateAlertSensitivityRequest": { + "required": [ + "alertSensitivity" + ], + "type": "object", + "properties": { + "alertSensitivity": { + "minLength": 1, + "pattern": "ALL|INCIDENTS_ONLY|MAJOR_ONLY", + "type": "string", + "description": "Alert sensitivity: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents)" + } + }, + "description": "Request body for updating alert sensitivity on a service subscription" + }, + "UpdateApiKeyRequest": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 200, + "minLength": 0, + "type": "string", + "description": "New name for this API key" + } + } + }, + "TableValueResultWorkspaceDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkspaceDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseMapStringString": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "SingleValueResponseListMonitorAssertionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitorAssertionDto" + } + } + } + }, + "SchedulableMonitorDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "type": { + "type": "string", + "enum": [ + "HTTP", + "DNS", + "MCP_SERVER", + "TCP", + "ICMP", + "HEARTBEAT" + ] + }, + "config": { + "oneOf": [ + { + "$ref": "#/components/schemas/DnsMonitorConfig" + }, + { + "$ref": "#/components/schemas/HeartbeatMonitorConfig" + }, + { + "$ref": "#/components/schemas/HttpMonitorConfig" + }, + { + "$ref": "#/components/schemas/IcmpMonitorConfig" + }, + { + "$ref": "#/components/schemas/McpServerMonitorConfig" + }, + { + "$ref": "#/components/schemas/TcpMonitorConfig" + } + ] + }, + "frequencySeconds": { + "type": "integer", + "format": "int32" + }, + "regions": { + "type": "array", + "items": { + "type": "string" + } + }, + "organizationId": { + "type": "integer", + "format": "int32" + } + } + }, + "TableValueResultAdapterHealthDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdapterHealthDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseListBillingPlanDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BillingPlanDto" + } + } + } + }, + "TableValueResultTransactionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultSubscriptionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubscriptionDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseUpcomingChargeResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/UpcomingChargeResponse" + } + } + }, + "UpcomingChargeResponse": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "UPGRADE", + "DOWNGRADE", + "NOOP" + ] + }, + "immediateAmount": { + "type": "integer", + "format": "int32" + }, + "nextBillingAmount": { + "type": "integer", + "format": "int32" + }, + "nextBillingDate": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "EntitlementDto": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Entitlement key" + }, + "value": { + "type": "integer", + "description": "Effective limit value (overrides applied)", + "format": "int64" + }, + "defaultValue": { + "type": "integer", + "description": "Plan-tier default value before overrides", + "format": "int64" + }, + "overridden": { + "type": "boolean", + "description": "Whether this entitlement has an org-level override" + } + }, + "description": "A single resolved entitlement for the organization" + }, + "EntitlementResponse": { + "type": "object", + "properties": { + "tier": { + "type": "string", + "description": "Resolved billing plan tier", + "enum": [ + "FREE", + "STARTER", + "PRO", + "TEAM", + "BUSINESS", + "ENTERPRISE" + ] + }, + "entitlements": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/EntitlementDto" + }, + "description": "All entitlements keyed by entitlement key" + }, + "usage": { + "type": "object", + "additionalProperties": { + "type": "integer", + "description": "Current usage counters keyed by entitlement key (only for countable resources)", + "format": "int64" + }, + "description": "Current usage counters keyed by entitlement key (only for countable resources)" + }, + "trialActive": { + "type": "boolean", + "description": "Whether the org is currently on a trial" + }, + "trialExpiresAt": { + "type": "string", + "description": "Trial expiry date (null if not trialing)", + "format": "date-time", + "nullable": true + }, + "subscriptionStatus": { + "type": "string", + "description": "Current subscription status (null if no subscription)", + "nullable": true + } + }, + "description": "Full entitlement state for an organization: resolved limits, usage, and trial info" + }, + "SingleValueResponseEntitlementResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/EntitlementResponse" + } + } + }, + "PaginationParams": { + "required": [ + "sortBy", + "sortOrder" + ], + "type": "object", + "properties": { + "sortBy": { + "type": "string" + }, + "sortOrder": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ] + }, + "page": { + "minimum": 0, + "type": "integer", + "format": "int32" + }, + "size": { + "maximum": 200, + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + }, + "IdValuePair": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "value": { + "type": "string" + } + } + }, + "TableValueResultIdValuePair": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdValuePair" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "MyOrgItemDto": { + "type": "object", + "properties": { + "orgId": { + "type": "integer", + "format": "int32" + }, + "orgName": { + "type": "string" + }, + "orgRole": { + "type": "string", + "enum": [ + "OWNER", + "ADMIN", + "MEMBER" + ] + }, + "status": { + "type": "string", + "enum": [ + "INVITED", + "ACTIVE", + "SUSPENDED", + "LEFT", + "REMOVED", + "DECLINED" + ] + } + } + }, + "TableValueResultMyOrgItemDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MyOrgItemDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SseEmitter": { + "type": "object", + "properties": { + "timeout": { + "type": "integer", + "format": "int64", + "nullable": true + } + } + }, + "Pageable": { + "type": "object", + "properties": { + "page": { + "minimum": 0, + "type": "integer", + "format": "int32" + }, + "size": { + "minimum": 1, + "type": "integer", + "format": "int32" + }, + "sort": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "TableValueResultUserDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "AdminStatsDto": { + "type": "object", + "properties": { + "userCount": { + "type": "integer", + "format": "int64" + }, + "orgCount": { + "type": "integer", + "format": "int64" + }, + "memberCount": { + "type": "integer", + "format": "int64" + } + } + }, + "SingleValueResponseAdminStatsDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AdminStatsDto" + } + } + }, + "TableValueResultOrganizationDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultMemberDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MemberDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultWebhookEndpointDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookEndpointDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultWebhookDeliveryDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookDeliveryDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "WebhookDeliveryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "endpointId": { + "type": "string", + "format": "uuid" + }, + "eventId": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "attemptCount": { + "type": "integer", + "format": "int32" + }, + "maxAttempts": { + "type": "integer", + "format": "int32" + }, + "responseStatus": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "responseLatencyMs": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "errorMessage": { + "type": "string", + "nullable": true + }, + "deliveredAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "failedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "nextRetryAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "SingleValueResponseWebhookSigningSecretDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/WebhookSigningSecretDto" + } + } + }, + "WebhookSigningSecretDto": { + "type": "object", + "properties": { + "configured": { + "type": "boolean" + }, + "maskedSecret": { + "type": "string", + "nullable": true + } + } + }, + "WebhookEventCatalogEntry": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Dot-notation event type identifier, e.g. \"monitor.created\"" + }, + "surface": { + "type": "string", + "description": "Product surface this event belongs to, e.g. \"monitoring\" or \"status_data\"" + }, + "description": { + "type": "string", + "description": "Human-readable description of when this event fires" + } + } + }, + "WebhookEventCatalogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookEventCatalogEntry" + } + } + } + }, + "CursorPageServiceCatalogDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "description": "Items on this page", + "items": { + "$ref": "#/components/schemas/ServiceCatalogDto" + } + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page; null when there are no more results", + "nullable": true + }, + "hasMore": { + "type": "boolean", + "description": "Whether more results exist beyond this page" + } + }, + "description": "Cursor-paginated response for time-series and append-only data" + }, + "ServiceCatalogDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string", + "nullable": true + }, + "officialStatusUrl": { + "type": "string", + "nullable": true + }, + "developerContext": { + "type": "string", + "nullable": true + }, + "logoUrl": { + "type": "string", + "nullable": true + }, + "adapterType": { + "type": "string" + }, + "pollingIntervalSeconds": { + "type": "integer", + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "overallStatus": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "componentCount": { + "type": "integer", + "format": "int64" + }, + "activeIncidentCount": { + "type": "integer", + "format": "int64" + }, + "dataCompleteness": { + "type": "string" + } + }, + "description": "Items on this page" + }, + "MaintenanceComponentRef": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "MaintenanceUpdateDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "displayAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "description": "A status update within a scheduled maintenance lifecycle" + }, + "ScheduledMaintenanceDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "externalId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "impact": { + "type": "string", + "nullable": true + }, + "shortlink": { + "type": "string", + "nullable": true + }, + "scheduledFor": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "scheduledUntil": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "affectedComponents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MaintenanceComponentRef" + } + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MaintenanceUpdateDto" + } + } + }, + "description": "A scheduled maintenance window from a vendor status page" + }, + "ServiceDetailDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string", + "nullable": true + }, + "officialStatusUrl": { + "type": "string", + "nullable": true + }, + "developerContext": { + "type": "string", + "nullable": true + }, + "logoUrl": { + "type": "string", + "nullable": true + }, + "adapterType": { + "type": "string" + }, + "pollingIntervalSeconds": { + "type": "integer", + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "currentStatus": { + "$ref": "#/components/schemas/ServiceStatusDto" + }, + "recentIncidents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceIncidentDto" + } + }, + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceComponentDto" + } + }, + "uptime": { + "$ref": "#/components/schemas/ComponentUptimeSummaryDto" + }, + "activeMaintenances": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduledMaintenanceDto" + } + }, + "dataCompleteness": { + "type": "string" + } + } + }, + "ServiceIncidentDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "serviceId": { + "type": "string", + "format": "uuid" + }, + "serviceSlug": { + "type": "string", + "nullable": true + }, + "serviceName": { + "type": "string", + "nullable": true + }, + "externalId": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "impact": { + "type": "string", + "nullable": true + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resolvedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "shortlink": { + "type": "string", + "nullable": true + }, + "detectedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "vendorCreatedAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "ServiceStatusDto": { + "type": "object", + "properties": { + "overallStatus": { + "type": "string" + }, + "lastPolledAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseServiceDetailDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ServiceDetailDto" + } + } + }, + "ServiceUptimeResponse": { + "type": "object", + "properties": { + "overallUptimePct": { + "type": "number", + "description": "Overall uptime percentage across the entire period; null when no polling data exists", + "format": "double", + "nullable": true, + "example": 99.95 + }, + "period": { + "type": "string", + "description": "Requested period", + "example": "7d" + }, + "granularity": { + "type": "string", + "description": "Requested granularity", + "example": "hourly" + }, + "buckets": { + "type": "array", + "description": "Per-bucket breakdown ordered by time ascending", + "items": { + "$ref": "#/components/schemas/UptimeBucketDto" + } + }, + "source": { + "type": "string", + "description": "Data source: vendor_reported, incident_derived, or poll_derived", + "nullable": true, + "example": "vendor_reported" + } + }, + "description": "Uptime response with per-bucket breakdown and overall percentage for the period" + }, + "SingleValueResponseServiceUptimeResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ServiceUptimeResponse" + } + } + }, + "UptimeBucketDto": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "description": "Start of the bucket interval (ISO 8601)", + "format": "date-time", + "example": "2024-01-01T00:00:00Z" + }, + "uptimePct": { + "type": "number", + "description": "Uptime percentage for this bucket; null when no polls occurred", + "format": "double", + "nullable": true, + "example": 100.0 + }, + "totalPolls": { + "type": "integer", + "description": "Total number of polls recorded in this bucket", + "format": "int64", + "example": 12 + } + }, + "description": "Uptime statistics for a single time bucket" + }, + "TableValueResultScheduledMaintenanceDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduledMaintenanceDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultServiceIncidentDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceIncidentDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "ServiceIncidentDetailDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string" + }, + "status": { + "type": "string" + }, + "impact": { + "type": "string", + "nullable": true + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "resolvedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "detectedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "shortlink": { + "type": "string", + "nullable": true + }, + "affectedComponents": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceIncidentUpdateDto" + } + } + } + }, + "ServiceIncidentUpdateDto": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "displayAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, + "SingleValueResponseServiceIncidentDetailDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ServiceIncidentDetailDto" + } + } + }, + "TableValueResultServiceComponentDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceComponentDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "ComponentUptimeDayDto": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "partialOutageSeconds": { + "type": "integer", + "format": "int32" + }, + "majorOutageSeconds": { + "type": "integer", + "format": "int32" + }, + "uptimePercentage": { + "type": "number", + "format": "double" + }, + "eventsJson": { + "type": "string", + "description": "Incident event references for this day as raw JSON", + "nullable": true + }, + "source": { + "type": "string" + } + }, + "description": "Daily uptime data for a component" + }, + "TableValueResultComponentUptimeDayDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentUptimeDayDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "GlobalStatusSummaryDto": { + "type": "object", + "properties": { + "totalServices": { + "type": "integer", + "format": "int32" + }, + "operationalCount": { + "type": "integer", + "format": "int32" + }, + "degradedCount": { + "type": "integer", + "format": "int32" + }, + "partialOutageCount": { + "type": "integer", + "format": "int32" + }, + "majorOutageCount": { + "type": "integer", + "format": "int32" + }, + "maintenanceCount": { + "type": "integer", + "format": "int32" + }, + "activeIncidentCount": { + "type": "integer", + "format": "int64" + }, + "servicesWithIssues": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceCatalogDto" + } + } + } + }, + "SingleValueResponseGlobalStatusSummaryDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/GlobalStatusSummaryDto" + } + } + }, + "TableValueResultServiceSubscriptionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceSubscriptionDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultSecretDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SecretDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultResourceGroupDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceGroupDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseResourceGroupHealthDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ResourceGroupHealthDto" + } + } + }, + "NotificationDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "body": { + "type": "string", + "nullable": true + }, + "resourceType": { + "type": "string", + "nullable": true + }, + "resourceId": { + "type": "string", + "nullable": true + }, + "read": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "TableValueResultNotificationDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseLong": { + "type": "object", + "properties": { + "data": { + "type": "integer", + "format": "int64" + } + } + }, + "TableValueResultNotificationPolicyDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationPolicyDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultNotificationDispatchDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationDispatchDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultMonitorDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitorDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "MonitorVersionDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "monitorId": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer", + "format": "int32" + }, + "snapshot": { + "$ref": "#/components/schemas/MonitorDto" + }, + "changedById": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "changedVia": { + "type": "string", + "enum": [ + "API", + "DASHBOARD", + "CLI", + "TERRAFORM" + ] + }, + "changeSummary": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "TableValueResultMonitorVersionDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MonitorVersionDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "SingleValueResponseMonitorVersionDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MonitorVersionDto" + } + } + }, + "UptimeDto": { + "type": "object", + "properties": { + "uptimePercentage": { + "type": "number", + "description": "Uptime percentage over the requested window; null when no data", + "format": "double", + "nullable": true, + "example": 99.95 + }, + "totalChecks": { + "type": "integer", + "description": "Total number of checks executed", + "format": "int64", + "example": 1440 + }, + "passedChecks": { + "type": "integer", + "description": "Number of checks that passed", + "format": "int64", + "example": 1439 + }, + "avgLatencyMs": { + "type": "number", + "description": "Weighted average latency in milliseconds; null when no data", + "format": "double", + "nullable": true, + "example": 142.5 + }, + "p95LatencyMs": { + "type": "number", + "description": "95th-percentile latency in milliseconds (upper bound across regions); null when no data", + "format": "double", + "nullable": true, + "example": 312.0 + } + }, + "description": "Uptime statistics aggregated from continuous aggregates" + }, + "SingleValueResponseUptimeDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/UptimeDto" + } + } + }, + "CursorPage": { + "type": "object", + "properties": { + "data": { + "type": "array", + "description": "Items on this page", + "items": { + "type": "object", + "description": "Items on this page" + } + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page; null when there are no more results", + "nullable": true + }, + "hasMore": { + "type": "boolean", + "description": "Whether more results exist beyond this page" + } + }, + "description": "Cursor-paginated response for time-series and append-only data" + }, + "AssertionResultDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Assertion type", + "example": "status_code" + }, + "passed": { + "type": "boolean", + "description": "Whether the assertion passed" + }, + "severity": { + "type": "string", + "description": "Assertion severity", + "enum": [ + "fail", + "warn" + ] + }, + "message": { + "type": "string", + "description": "Human-readable result message", + "nullable": true + }, + "expected": { + "type": "string", + "description": "Expected value", + "nullable": true, + "example": "200" + }, + "actual": { + "type": "string", + "description": "Actual value observed", + "nullable": true, + "example": "503" + } + }, + "description": "Result of evaluating a single assertion against a check result" + }, + "CheckResultDetailsDto": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer", + "description": "HTTP status code of the response", + "format": "int32", + "nullable": true, + "example": 200 + }, + "responseHeaders": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "HTTP response headers", + "nullable": true, + "items": { + "type": "string", + "description": "HTTP response headers", + "nullable": true + } + }, + "description": "HTTP response headers", + "nullable": true + }, + "responseBodySnapshot": { + "type": "string", + "description": "Raw response body snapshot (may be HTML, XML, JSON, or plain text)", + "nullable": true + }, + "assertionResults": { + "type": "array", + "description": "Individual assertion evaluation results", + "nullable": true, + "items": { + "$ref": "#/components/schemas/AssertionResultDto" + } + }, + "tlsInfo": { + "$ref": "#/components/schemas/TlsInfoDto" + }, + "redirectCount": { + "type": "integer", + "description": "Number of HTTP redirects followed", + "format": "int32", + "nullable": true, + "example": 2 + }, + "redirectTarget": { + "type": "string", + "description": "Final URL after redirects", + "nullable": true + }, + "responseSizeBytes": { + "type": "integer", + "description": "Response body size in bytes", + "format": "int32", + "nullable": true, + "example": 4096 + }, + "checkDetails": { + "oneOf": [ + { + "$ref": "#/components/schemas/Dns" + }, + { + "$ref": "#/components/schemas/Http" + }, + { + "$ref": "#/components/schemas/Icmp" + }, + { + "$ref": "#/components/schemas/McpServer" + }, + { + "$ref": "#/components/schemas/Tcp" + } + ] + } + }, + "description": "Type-specific details captured during a check execution" + }, + "CheckResultDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the check result", + "format": "uuid" + }, + "timestamp": { + "type": "string", + "description": "Timestamp when the check was executed (ISO 8601)", + "format": "date-time" + }, + "region": { + "type": "string", + "description": "Region where the check was executed", + "example": "us-east" + }, + "responseTimeMs": { + "type": "integer", + "description": "Response time in milliseconds", + "format": "int32", + "nullable": true, + "example": 123 + }, + "passed": { + "type": "boolean", + "description": "Whether the check passed", + "example": true + }, + "failureReason": { + "type": "string", + "description": "Reason for failure when passed=false", + "nullable": true + }, + "severityHint": { + "type": "string", + "description": "Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", + "nullable": true + }, + "details": { + "$ref": "#/components/schemas/CheckResultDetailsDto" + } + }, + "description": "A single check result from a monitor run" + }, + "CheckTypeDetailsDto": { + "required": [ + "check_type" + ], + "type": "object", + "properties": { + "check_type": { + "type": "string" + } + }, + "description": "Check-type-specific details \u2014 polymorphic by check_type discriminator", + "discriminator": { + "propertyName": "check_type" + } + }, + "CursorPageCheckResultDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "description": "Items on this page", + "items": { + "$ref": "#/components/schemas/CheckResultDto" + } + }, + "nextCursor": { + "type": "string", + "description": "Opaque cursor for the next page; null when there are no more results", + "nullable": true + }, + "hasMore": { + "type": "boolean", + "description": "Whether more results exist beyond this page" + } + }, + "description": "Cursor-paginated response for time-series and append-only data" + }, + "Dns": { + "type": "object", + "description": "DNS check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "hostname": { + "type": "string", + "description": "Target hostname", + "nullable": true + }, + "requestedTypes": { + "type": "array", + "description": "Requested DNS record types", + "nullable": true, + "items": { + "type": "string", + "description": "Requested DNS record types", + "nullable": true + } + }, + "usedResolver": { + "type": "string", + "description": "Resolver used for lookup", + "nullable": true + }, + "records": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "Resolved DNS records keyed by record type", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Resolved DNS records keyed by record type", + "nullable": true + }, + "description": "Resolved DNS records keyed by record type", + "nullable": true + } + }, + "description": "Resolved DNS records keyed by record type", + "nullable": true + }, + "attempts": { + "type": "array", + "description": "DNS resolution attempts", + "nullable": true, + "items": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "DNS resolution attempts", + "nullable": true + }, + "description": "DNS resolution attempts", + "nullable": true + } + }, + "failureKind": { + "type": "string", + "description": "Kind of DNS failure, if any", + "nullable": true + } + } + } + ] + }, + "Http": { + "type": "object", + "description": "HTTP check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "timing": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Request phase timing breakdown", + "nullable": true + }, + "description": "Request phase timing breakdown", + "nullable": true + }, + "bodyTruncated": { + "type": "boolean", + "description": "Whether the response body was truncated before storage", + "nullable": true + } + } + } + ] + }, + "Icmp": { + "type": "object", + "description": "ICMP (ping) check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Target host", + "example": "1.1.1.1" + }, + "packetsSent": { + "type": "integer", + "description": "Number of ICMP packets sent", + "format": "int32", + "nullable": true + }, + "packetsReceived": { + "type": "integer", + "description": "Number of ICMP packets received", + "format": "int32", + "nullable": true + }, + "packetLoss": { + "type": "number", + "description": "Packet loss percentage", + "format": "double", + "nullable": true, + "example": 0.0 + }, + "avgRttMs": { + "type": "number", + "description": "Average round-trip time in ms", + "format": "double", + "nullable": true + }, + "minRttMs": { + "type": "number", + "description": "Minimum round-trip time in ms", + "format": "double", + "nullable": true + }, + "maxRttMs": { + "type": "number", + "description": "Maximum round-trip time in ms", + "format": "double", + "nullable": true + }, + "jitterMs": { + "type": "number", + "description": "Jitter in ms", + "format": "double", + "nullable": true + } + } + } + ] + }, + "McpServer": { + "type": "object", + "description": "MCP server check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "MCP server URL", + "nullable": true + }, + "protocolVersion": { + "type": "string", + "description": "MCP protocol version", + "nullable": true + }, + "serverInfo": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "description": "MCP server info (name, version, etc.)", + "nullable": true + }, + "toolCount": { + "type": "integer", + "description": "Number of tools exposed", + "format": "int32", + "nullable": true + }, + "resourceCount": { + "type": "integer", + "description": "Number of resources exposed", + "format": "int32", + "nullable": true + }, + "promptCount": { + "type": "integer", + "description": "Number of prompts exposed", + "format": "int32", + "nullable": true + } + } + } + ] + }, + "Tcp": { + "type": "object", + "description": "TCP check-type-specific details", + "allOf": [ + { + "$ref": "#/components/schemas/CheckTypeDetailsDto" + }, + { + "type": "object", + "properties": { + "host": { + "type": "string", + "description": "Target host", + "example": "db.example.com" + }, + "port": { + "type": "integer", + "description": "Target port", + "format": "int32", + "example": 5432 + }, + "connected": { + "type": "boolean", + "description": "Whether a TCP connection was established" + } + } + } + ] + }, + "TlsInfoDto": { + "type": "object", + "properties": { + "subjectCn": { + "type": "string", + "description": "Certificate subject common name", + "nullable": true, + "example": "*.example.com" + }, + "subjectSan": { + "type": "array", + "description": "Subject Alternative Names", + "nullable": true, + "items": { + "type": "string", + "description": "Subject Alternative Names", + "nullable": true + } + }, + "issuerCn": { + "type": "string", + "description": "Issuer common name", + "nullable": true, + "example": "R3" + }, + "issuerOrg": { + "type": "string", + "description": "Issuer organisation", + "nullable": true, + "example": "Let's Encrypt" + }, + "notBefore": { + "type": "string", + "description": "Certificate validity start (ISO 8601 UTC)", + "nullable": true + }, + "notAfter": { + "type": "string", + "description": "Certificate validity end (ISO 8601 UTC)", + "nullable": true + }, + "serialNumber": { + "type": "string", + "description": "Certificate serial number", + "nullable": true + }, + "tlsVersion": { + "type": "string", + "description": "TLS protocol version", + "nullable": true, + "example": "TLSv1.3" + }, + "cipherSuite": { + "type": "string", + "description": "Negotiated cipher suite", + "nullable": true + }, + "chainValid": { + "type": "boolean", + "description": "Whether the chain validated against the OS trust store", + "nullable": true + } + }, + "description": "TLS/SSL certificate details for HTTPS targets" + }, + "ChartBucketDto": { + "type": "object", + "properties": { + "bucket": { + "type": "string", + "description": "Start of the time bucket (ISO 8601)", + "format": "date-time", + "example": "2026-03-12T10:00:00Z" + }, + "uptimePercent": { + "type": "number", + "description": "Uptime percentage for this bucket; null when no data", + "format": "double", + "nullable": true, + "example": 100.0 + }, + "avgLatencyMs": { + "type": "number", + "description": "Weighted average latency in milliseconds for this bucket", + "format": "double", + "nullable": true, + "example": 120.3 + }, + "p95LatencyMs": { + "type": "number", + "description": "95th percentile latency in milliseconds (max across regions)", + "format": "double", + "nullable": true, + "example": 250.0 + }, + "p99LatencyMs": { + "type": "number", + "description": "99th percentile latency in milliseconds (max across regions)", + "format": "double", + "nullable": true, + "example": 480.0 + } + }, + "description": "Aggregated metrics for a time bucket" + }, + "RegionStatusDto": { + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "Region identifier", + "example": "us-east" + }, + "passed": { + "type": "boolean", + "description": "Whether the last check in this region passed", + "example": true + }, + "responseTimeMs": { + "type": "integer", + "description": "Response time in milliseconds for the last check", + "format": "int32", + "nullable": true, + "example": 95 + }, + "timestamp": { + "type": "string", + "description": "Timestamp of the last check in this region (ISO 8601)", + "format": "date-time" + }, + "severityHint": { + "type": "string", + "description": "Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", + "nullable": true + } + }, + "description": "Latest check result for a single region" + }, + "ResultSummaryDto": { + "type": "object", + "properties": { + "currentStatus": { + "type": "string", + "description": "Derived current status across all regions", + "enum": [ + "up", + "degraded", + "down", + "unknown" + ] + }, + "latestPerRegion": { + "type": "array", + "description": "Latest check result per region", + "items": { + "$ref": "#/components/schemas/RegionStatusDto" + } + }, + "chartData": { + "type": "array", + "description": "Time-bucketed chart data for the requested window", + "items": { + "$ref": "#/components/schemas/ChartBucketDto" + } + }, + "uptime24h": { + "type": "number", + "description": "Uptime percentage over the last 24 hours; null when no data", + "format": "double", + "nullable": true, + "example": 99.95 + }, + "uptimeWindow": { + "type": "number", + "description": "Uptime percentage for the selected chart window; null when no data", + "format": "double", + "nullable": true, + "example": 99.8 + } + }, + "description": "Dashboard summary: current status, per-region latest results, and chart data" + }, + "SingleValueResponseResultSummaryDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ResultSummaryDto" + } + } + }, + "TableValueResultMaintenanceWindowDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MaintenanceWindowDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultInviteDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InviteDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "IntegrationCatalogResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IntegrationDto" + } + } + } + }, + "IntegrationConfigSchemaDto": { + "type": "object", + "properties": { + "connectionFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IntegrationFieldDto" + } + }, + "channelFields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IntegrationFieldDto" + } + } + } + }, + "IntegrationDto": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "authType": { + "type": "string" + }, + "tierAvailability": { + "type": "string", + "enum": [ + "FREE", + "STARTER", + "PRO", + "TEAM", + "BUSINESS", + "ENTERPRISE" + ] + }, + "lifecycle": { + "type": "string" + }, + "setupGuideUrl": { + "type": "string" + }, + "configSchema": { + "$ref": "#/components/schemas/IntegrationConfigSchemaDto" + } + } + }, + "IntegrationFieldDto": { + "required": [ + "key", + "label", + "required", + "sensitive", + "type" + ], + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "type": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sensitive": { + "type": "boolean" + }, + "placeholder": { + "type": "string", + "nullable": true + }, + "helpText": { + "type": "string", + "nullable": true + }, + "options": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "nullable": true + } + }, + "default": { + "type": "string", + "nullable": true + } + } + }, + "IncidentFilterParams": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "WATCHING", + "TRIGGERED", + "CONFIRMED", + "RESOLVED" + ] + }, + "severity": { + "type": "string", + "enum": [ + "DOWN", + "DEGRADED", + "MAINTENANCE" + ] + }, + "source": { + "type": "string", + "enum": [ + "AUTOMATIC", + "MANUAL", + "MONITORS", + "STATUS_DATA", + "RESOURCE_GROUP" + ] + }, + "monitorId": { + "type": "string", + "format": "uuid" + }, + "serviceId": { + "type": "string", + "format": "uuid" + }, + "resourceGroupId": { + "type": "string", + "format": "uuid" + }, + "tagId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "environmentId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "startedFrom": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "startedTo": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "page": { + "minimum": 0, + "type": "integer", + "format": "int32" + }, + "size": { + "maximum": 200, + "minimum": 1, + "type": "integer", + "format": "int32" + } + } + }, + "TableValueResultEnvironmentDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EnvironmentDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "DashboardOverviewDto": { + "type": "object", + "properties": { + "monitors": { + "$ref": "#/components/schemas/MonitorsSummaryDto" + }, + "incidents": { + "$ref": "#/components/schemas/IncidentsSummaryDto" + } + } + }, + "IncidentsSummaryDto": { + "type": "object", + "properties": { + "active": { + "type": "integer", + "format": "int64" + }, + "resolvedToday": { + "type": "integer", + "format": "int64" + }, + "mttr30d": { + "type": "number", + "format": "double", + "nullable": true + } + } + }, + "MonitorsSummaryDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "up": { + "type": "integer", + "format": "int64" + }, + "down": { + "type": "integer", + "format": "int64" + }, + "degraded": { + "type": "integer", + "format": "int64" + }, + "paused": { + "type": "integer", + "format": "int64" + }, + "avgUptime24h": { + "type": "number", + "format": "double", + "nullable": true + }, + "avgUptime30d": { + "type": "number", + "format": "double", + "nullable": true + } + } + }, + "SingleValueResponseDashboardOverviewDto": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/DashboardOverviewDto" + } + } + }, + "CategoryDto": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "serviceCount": { + "type": "integer", + "format": "int64" + } + } + }, + "TableValueResultCategoryDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CategoryDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "AuthMeResponse": { + "type": "object", + "properties": { + "key": { + "$ref": "#/components/schemas/KeyInfo" + }, + "organization": { + "$ref": "#/components/schemas/OrgInfo" + }, + "plan": { + "$ref": "#/components/schemas/PlanInfo" + }, + "rateLimits": { + "$ref": "#/components/schemas/RateLimitInfo" + } + }, + "description": "Identity, organization, plan, and rate-limit info for the authenticated API key" + }, + "KeyInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Key ID", + "format": "int32" + }, + "name": { + "type": "string", + "description": "Human-readable key name" + }, + "createdAt": { + "type": "string", + "description": "When the key was created", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "description": "When the key expires (null = never)", + "format": "date-time", + "nullable": true + }, + "lastUsedAt": { + "type": "string", + "description": "Last time the key was used", + "format": "date-time", + "nullable": true + } + }, + "description": "API key metadata" + }, + "OrgInfo": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Organization ID", + "format": "int32" + }, + "name": { + "type": "string", + "description": "Organization name" + } + }, + "description": "Organization the key belongs to" + }, + "PlanInfo": { + "type": "object", + "properties": { + "tier": { + "type": "string", + "description": "Resolved plan tier", + "enum": [ + "FREE", + "STARTER", + "PRO", + "TEAM", + "BUSINESS", + "ENTERPRISE" + ] + }, + "subscriptionStatus": { + "type": "string", + "description": "Subscription status (null if no subscription)", + "nullable": true + }, + "trialActive": { + "type": "boolean", + "description": "Whether the org is on a trial" + }, + "trialExpiresAt": { + "type": "string", + "description": "Trial expiry (null if not trialing)", + "format": "date-time", + "nullable": true + }, + "entitlements": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/EntitlementDto" + }, + "description": "Entitlement limits keyed by entitlement name" + }, + "usage": { + "type": "object", + "additionalProperties": { + "type": "integer", + "description": "Current usage counters keyed by entitlement name", + "format": "int64" + }, + "description": "Current usage counters keyed by entitlement name" + } + }, + "description": "Billing plan and entitlement state" + }, + "RateLimitInfo": { + "type": "object", + "properties": { + "requestsPerMinute": { + "type": "integer", + "description": "Maximum requests allowed per window", + "format": "int64" + }, + "remaining": { + "type": "integer", + "description": "Requests remaining in the current window", + "format": "int64" + }, + "windowMs": { + "type": "integer", + "description": "Sliding window size in milliseconds", + "format": "int64" + } + }, + "description": "Rate-limit quota for the current sliding window" + }, + "SingleValueResponseAuthMeResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/AuthMeResponse" + } + } + }, + "AuditEventDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "actorId": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "actorEmail": { + "type": "string", + "nullable": true + }, + "action": { + "type": "string" + }, + "resourceType": { + "type": "string", + "nullable": true + }, + "resourceId": { + "type": "string", + "nullable": true + }, + "resourceName": { + "type": "string", + "nullable": true + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "object", + "nullable": true + }, + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "PageResultAuditEventDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditEventDto" + } + }, + "page": { + "type": "integer", + "format": "int32" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "totalElements": { + "type": "integer", + "format": "int64" + }, + "totalPages": { + "type": "integer", + "format": "int32" + }, + "hasNext": { + "type": "boolean" + } + } + }, + "TableValueResultApiKeyDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApiKeyDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "DeliveryAttemptDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "deliveryId": { + "type": "string", + "format": "uuid" + }, + "attemptNumber": { + "type": "integer", + "description": "1-based attempt number", + "format": "int32" + }, + "status": { + "type": "string", + "description": "Outcome: SUCCESS, FAILED, TIMEOUT, ERROR" + }, + "responseStatusCode": { + "type": "integer", + "description": "HTTP response status code from the external service", + "format": "int32", + "nullable": true + }, + "requestPayload": { + "type": "string", + "description": "JSON payload sent to the external service", + "nullable": true + }, + "responseBody": { + "type": "string", + "description": "Response body from the external service (truncated)", + "nullable": true + }, + "errorMessage": { + "type": "string", + "description": "Error message if the attempt failed", + "nullable": true + }, + "responseTimeMs": { + "type": "integer", + "description": "Round-trip time in milliseconds", + "format": "int32", + "nullable": true + }, + "externalId": { + "type": "string", + "description": "External identifier (e.g. PagerDuty dedup_key, SES MessageId, webhook delivery UUID)", + "nullable": true + }, + "requestHeaders": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "HTTP request headers sent to the external service", + "nullable": true + }, + "description": "HTTP request headers sent to the external service", + "nullable": true + }, + "attemptedAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "Single delivery attempt with request/response audit data" + }, + "TableValueResultDeliveryAttemptDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryAttemptDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultAlertChannelDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertChannelDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "TableValueResultAlertDeliveryDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AlertDeliveryDto" + } + }, + "hasNext": { + "type": "boolean" + }, + "hasPrev": { + "type": "boolean" + } + } + }, + "RemoveMonitorTagsRequest": { + "required": [ + "tagIds" + ], + "type": "object", + "properties": { + "tagIds": { + "minItems": 1, + "type": "array", + "description": "IDs of the tags to detach from the monitor", + "items": { + "type": "string", + "description": "IDs of the tags to detach from the monitor", + "format": "uuid" + } + } + }, + "description": "Request body for removing tags from a monitor" + }, + "DeleteChannelResult": { + "type": "object", + "properties": { + "affectedPolicies": { + "type": "integer", + "description": "Number of notification policies whose escalation steps were modified", + "format": "int32" + }, + "disabledPolicies": { + "type": "integer", + "description": "Number of notification policies disabled because they had no remaining channels", + "format": "int32" + } + }, + "description": "Summary of policies affected by channel deletion" + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "description": "API key (dh_live_...) or Auth0 JWT token", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} \ No newline at end of file diff --git a/src/commands/alert-channels/test.ts b/src/commands/alert-channels/test.ts index 876deef..5d6d460 100644 --- a/src/commands/alert-channels/test.ts +++ b/src/commands/alert-channels/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class AlertChannelsTest extends Command { static description = 'Send a test notification to an alert channel' @@ -11,7 +11,7 @@ export default class AlertChannelsTest extends Command { async run() { const {args, flags} = await this.parse(AlertChannelsTest) const client = buildClient(flags) - const resp = await typedPost<{data?: {success?: boolean}}>(client, `/api/v1/alert-channels/${args.id}/test`) + const resp = await checkedFetch(client.POST('/api/v1/alert-channels/{id}/test', {params: {path: {id: args.id}}})) this.log(resp.data?.success ? 'Test notification sent successfully.' : 'Test notification failed.') } } diff --git a/src/commands/api-keys/revoke.ts b/src/commands/api-keys/revoke.ts index b2ef210..13d29b9 100644 --- a/src/commands/api-keys/revoke.ts +++ b/src/commands/api-keys/revoke.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class ApiKeysRevoke extends Command { static description = 'Revoke an API key' @@ -11,7 +11,7 @@ export default class ApiKeysRevoke extends Command { async run() { const {args, flags} = await this.parse(ApiKeysRevoke) const client = buildClient(flags) - await typedPost(client, `/api/v1/api-keys/${args.id}/revoke`) + await checkedFetch(client.POST('/api/v1/api-keys/{id}/revoke', {params: {path: {id: Number(args.id)}}})) this.log(`API key '${args.id}' revoked.`) } } diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 9d31e5c..f6f0cd1 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -1,7 +1,6 @@ import {Command, Flags} from '@oclif/core' import {globalFlags} from '../../lib/base-command.js' -import {createApiClient} from '../../lib/api-client.js' -import {typedGet} from '../../lib/typed-api.js' +import {createApiClient, checkedFetch, apiGet} from '../../lib/api-client.js' import {saveContext, resolveApiUrl} from '../../lib/auth.js' import * as readline from 'node:readline' @@ -26,9 +25,7 @@ export default class AuthLogin extends Command { const client = createApiClient({baseUrl: apiUrl, token}) try { - const resp = await typedGet<{data?: {organization?: {name?: string; id?: number}; key?: {name?: string}; plan?: {tier?: string}}}>( - client, '/api/v1/auth/me', - ) + const resp = await checkedFetch(client.GET('/api/v1/auth/me')) const me = resp.data saveContext({name: flags.name, apiUrl, token}, true) @@ -45,7 +42,7 @@ export default class AuthLogin extends Command { } try { - await typedGet(client, '/api/v1/dashboard/overview') + await apiGet(client, '/api/v1/dashboard/overview') saveContext({name: flags.name, apiUrl, token}, true) this.log('') this.log(` Authenticated successfully.`) diff --git a/src/commands/auth/me.ts b/src/commands/auth/me.ts index faa65f4..3c9c09e 100644 --- a/src/commands/auth/me.ts +++ b/src/commands/auth/me.ts @@ -1,24 +1,8 @@ import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedGet} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' import {formatOutput, OutputFormat} from '../../lib/output.js' -interface AuthMeResponse { - data?: { - key?: {id?: string; name?: string; createdAt?: string; expiresAt?: string; lastUsedAt?: string} - organization?: {id?: number; name?: string} - plan?: { - tier?: string - subscriptionStatus?: string - trialActive?: boolean - trialExpiresAt?: string - usage?: Record - entitlements?: Record - } - rateLimits?: {requestsPerMinute?: number; remaining?: number; windowMs?: number} - } -} - export default class AuthMe extends Command { static description = 'Show current API key identity, organization, plan, and rate limits' static examples = ['<%= config.bin %> auth me', '<%= config.bin %> auth me --output json'] @@ -27,7 +11,7 @@ export default class AuthMe extends Command { async run() { const {flags} = await this.parse(AuthMe) const client = buildClient(flags) - const resp = await typedGet(client, '/api/v1/auth/me') + const resp = await checkedFetch(client.GET('/api/v1/auth/me')) const me = resp.data const format = flags.output as OutputFormat diff --git a/src/commands/data/services/status.ts b/src/commands/data/services/status.ts index acd5d23..bcf4d4a 100644 --- a/src/commands/data/services/status.ts +++ b/src/commands/data/services/status.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' -import {typedGet} from '../../../lib/typed-api.js' +import {checkedFetch} from '../../../lib/api-client.js' export default class DataServicesStatus extends Command { static description = 'Get the current status of a service' @@ -11,7 +11,7 @@ export default class DataServicesStatus extends Command { async run() { const {args, flags} = await this.parse(DataServicesStatus) const client = buildClient(flags) - const resp = await typedGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}`) + const resp = await checkedFetch(client.GET('/api/v1/services/{slugOrId}', {params: {path: {slugOrId: args.slug}}})) display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/data/services/uptime.ts b/src/commands/data/services/uptime.ts index 1b6410b..ed8010a 100644 --- a/src/commands/data/services/uptime.ts +++ b/src/commands/data/services/uptime.ts @@ -1,6 +1,6 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../../lib/base-command.js' -import {typedGet} from '../../../lib/typed-api.js' +import {apiGet} from '../../../lib/api-client.js' export default class DataServicesUptime extends Command { static description = 'Get uptime data for a service' @@ -18,9 +18,9 @@ export default class DataServicesUptime extends Command { async run() { const {args, flags} = await this.parse(DataServicesUptime) const client = buildClient(flags) - const query: Record = {period: flags.period} + const query: Record = {period: flags.period} if (flags.granularity) query.granularity = flags.granularity - const resp = await typedGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}/uptime`, query) + const resp = await apiGet<{data?: unknown}>(client, `/api/v1/services/${args.slug}/uptime`, {query}) display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/dependencies/track.ts b/src/commands/dependencies/track.ts index d92033f..fd520f5 100644 --- a/src/commands/dependencies/track.ts +++ b/src/commands/dependencies/track.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class DependenciesTrack extends Command { static description = 'Start tracking a service as a dependency' @@ -11,7 +11,7 @@ export default class DependenciesTrack extends Command { async run() { const {args, flags} = await this.parse(DependenciesTrack) const client = buildClient(flags) - const resp = await typedPost<{data?: {serviceName?: string}}>(client, `/api/v1/service-subscriptions/${args.slug}`) - this.log(`Now tracking '${resp.data?.serviceName}' as a dependency.`) + const resp = await checkedFetch(client.POST('/api/v1/service-subscriptions/{slug}', {params: {path: {slug: args.slug}}})) + this.log(`Now tracking '${resp.data?.name}' as a dependency.`) } } diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 1f7f1b7..dc1c9fb 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -1,5 +1,6 @@ +import {hostname} from 'node:os' import {Command, Flags} from '@oclif/core' -import {createApiClient} from '../lib/api-client.js' +import {createApiClient, apiPost, apiDelete} from '../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../lib/auth.js' import {loadConfig, validate, fetchAllRefs, diff, formatPlan, apply, writeState, buildState} from '../lib/yaml/index.js' import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' @@ -35,6 +36,14 @@ export default class Deploy extends Command { description: 'Show what would change without applying (same as "devhelm plan")', default: false, }), + 'force-unlock': Flags.boolean({ + description: 'Force-break an existing deploy lock before acquiring', + default: false, + }), + 'no-lock': Flags.boolean({ + description: 'Skip deploy locking (not recommended for team use)', + default: false, + }), 'api-url': Flags.string({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), @@ -111,27 +120,76 @@ export default class Deploy extends Command { } } - this.log('Applying changes...') - const applyResult = await apply(changeset, refs, client) - - for (const s of applyResult.succeeded) { - const icon = s.action === 'delete' ? '-' : s.action === 'update' ? '~' : '+' - this.log(` ${icon} ${s.resourceType} "${s.refKey}" — ${s.action}d`) + let lockId: string | undefined + if (!flags['no-lock']) { + lockId = await this.acquireLock(client, flags['force-unlock']) } - if (applyResult.failed.length > 0) { - this.log('') - for (const f of applyResult.failed) { - this.log(` ✗ ${f.resourceType} "${f.refKey}" — ${f.action} failed: ${f.error}`) + try { + this.log('Applying changes...') + const applyResult = await apply(changeset, refs, client) + + for (const s of applyResult.succeeded) { + const icon = s.action === 'delete' ? '-' : s.action === 'update' ? '~' : '+' + this.log(` ${icon} ${s.resourceType} "${s.refKey}" — ${s.action}d`) } - } - writeState(buildState(applyResult.stateEntries)) + if (applyResult.failed.length > 0) { + this.log('') + for (const f of applyResult.failed) { + this.log(` ✗ ${f.resourceType} "${f.refKey}" — ${f.action} failed: ${f.error}`) + } + } - this.log(`\nDone: ${applyResult.succeeded.length} succeeded, ${applyResult.failed.length} failed.`) + writeState(buildState(applyResult.stateEntries)) - if (applyResult.failed.length > 0) { - this.exit(2) + this.log(`\nDone: ${applyResult.succeeded.length} succeeded, ${applyResult.failed.length} failed.`) + + if (applyResult.failed.length > 0) { + this.exit(2) + } + } finally { + if (lockId) { + await this.releaseLock(client, lockId) + } } } + + private async acquireLock(client: ReturnType, forceUnlock: boolean): Promise { + if (forceUnlock) { + try { + await apiDelete(client, '/api/v1/deploy/lock/force') + } catch { + // Force-unlock is best-effort; the lock may not exist + } + } + + try { + const resp = await apiPost<{data?: {id?: string}}>( + client, '/api/v1/deploy/lock', + {lockedBy: `${process.env.USER ?? 'cli'}@${hostname()}`, ttlMinutes: 10}, + ) + const lockId = resp.data?.id + if (!lockId) { + this.warn('Deploy lock acquired but no lock ID returned. Proceeding without lock protection.') + } + return lockId + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (msg.includes('409') || msg.includes('Conflict') || msg.includes('lock held')) { + this.warn(`Deploy lock conflict: ${msg}`) + this.warn('Use --force-unlock to break the existing lock, or --no-lock to skip locking.') + this.exit(3) + } + this.warn(`Failed to acquire deploy lock: ${msg}`) + this.warn('Use --no-lock to skip locking if the lock service is unavailable.') + this.exit(3) + } + } + + private async releaseLock(client: ReturnType, lockId: string): Promise { + try { + await apiDelete(client, `/api/v1/deploy/lock/${lockId}`) + } catch { /* best-effort release */ } + } } diff --git a/src/commands/incidents/delete.ts b/src/commands/incidents/delete.ts deleted file mode 100644 index f542288..0000000 --- a/src/commands/incidents/delete.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {createDeleteCommand} from '../../lib/crud-commands.js' -import {INCIDENTS} from '../../lib/resources.js' - -export default createDeleteCommand(INCIDENTS) diff --git a/src/commands/incidents/resolve.ts b/src/commands/incidents/resolve.ts index f815c4d..84b9f23 100644 --- a/src/commands/incidents/resolve.ts +++ b/src/commands/incidents/resolve.ts @@ -1,6 +1,6 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class IncidentsResolve extends Command { static description = 'Resolve an incident' @@ -15,7 +15,7 @@ export default class IncidentsResolve extends Command { const {args, flags} = await this.parse(IncidentsResolve) const client = buildClient(flags) const body = flags.message ? {message: flags.message} : undefined - const resp = await typedPost<{data?: {title?: string}}>(client, `/api/v1/incidents/${args.id}/resolve`, body) - this.log(`Incident '${resp.data?.title}' resolved.`) + const resp = await checkedFetch(client.POST('/api/v1/incidents/{id}/resolve', {params: {path: {id: args.id}}, body})) + this.log(`Incident '${resp.data?.incident?.title}' resolved.`) } } diff --git a/src/commands/incidents/update.ts b/src/commands/incidents/update.ts deleted file mode 100644 index e4e004e..0000000 --- a/src/commands/incidents/update.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {createUpdateCommand} from '../../lib/crud-commands.js' -import {INCIDENTS} from '../../lib/resources.js' - -export default createUpdateCommand(INCIDENTS) diff --git a/src/commands/monitors/pause.ts b/src/commands/monitors/pause.ts index 650bbcc..e54677b 100644 --- a/src/commands/monitors/pause.ts +++ b/src/commands/monitors/pause.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class MonitorsPause extends Command { static description = 'Pause a monitor' @@ -11,7 +11,7 @@ export default class MonitorsPause extends Command { async run() { const {args, flags} = await this.parse(MonitorsPause) const client = buildClient(flags) - const resp = await typedPost<{data?: {name?: string}}>(client, `/api/v1/monitors/${args.id}/pause`) + const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/pause', {params: {path: {id: args.id}}})) this.log(`Monitor '${resp.data?.name}' paused.`) } } diff --git a/src/commands/monitors/results.ts b/src/commands/monitors/results.ts index 9d1f07e..b4a169f 100644 --- a/src/commands/monitors/results.ts +++ b/src/commands/monitors/results.ts @@ -1,15 +1,9 @@ import {Command, Args, Flags} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' -import {typedGet} from '../../lib/typed-api.js' +import {apiGet} from '../../lib/api-client.js' +import type {components} from '../../lib/api.generated.js' -interface MonitorResult { - id?: string - status?: string - responseTime?: number - statusCode?: number - region?: string - checkedAt?: string -} +type CheckResultDto = components['schemas']['CheckResultDto'] export default class MonitorsResults extends Command { static description = 'Show recent check results for a monitor' @@ -23,14 +17,18 @@ export default class MonitorsResults extends Command { async run() { const {args, flags} = await this.parse(MonitorsResults) const client = buildClient(flags) - const resp = await typedGet<{data?: MonitorResult[]}>(client, `/api/v1/monitors/${args.id}/results`, {limit: flags.limit}) + const resp = await apiGet<{data?: CheckResultDto[]}>( + client, + `/api/v1/monitors/${args.id}/results`, + {query: {limit: flags.limit}}, + ) display(this, resp.data ?? [], flags.output, [ {header: 'ID', get: (r) => String(r.id ?? '')}, - {header: 'STATUS', get: (r) => String(r.status ?? '')}, - {header: 'RESPONSE TIME', get: (r) => String(r.responseTime ?? '')}, - {header: 'CODE', get: (r) => String(r.statusCode ?? '')}, + {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: 'CHECKED AT', get: (r) => String(r.checkedAt ?? '')}, + {header: 'TIMESTAMP', get: (r) => String(r.timestamp ?? '')}, ]) } } diff --git a/src/commands/monitors/resume.ts b/src/commands/monitors/resume.ts index 2cca88d..c84bfa6 100644 --- a/src/commands/monitors/resume.ts +++ b/src/commands/monitors/resume.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class MonitorsResume extends Command { static description = 'Resume a paused monitor' @@ -11,7 +11,7 @@ export default class MonitorsResume extends Command { async run() { const {args, flags} = await this.parse(MonitorsResume) const client = buildClient(flags) - const resp = await typedPost<{data?: {name?: string}}>(client, `/api/v1/monitors/${args.id}/resume`) + const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/resume', {params: {path: {id: args.id}}})) this.log(`Monitor '${resp.data?.name}' resumed.`) } } diff --git a/src/commands/monitors/test.ts b/src/commands/monitors/test.ts index f58ac8a..2d37af1 100644 --- a/src/commands/monitors/test.ts +++ b/src/commands/monitors/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class MonitorsTest extends Command { static description = 'Run an ad-hoc test for a monitor' @@ -12,7 +12,7 @@ export default class MonitorsTest extends Command { const {args, flags} = await this.parse(MonitorsTest) const client = buildClient(flags) this.log('Running test...') - const resp = await typedPost<{data?: Record}>(client, `/api/v1/monitors/${args.id}/test`) + const resp = await checkedFetch(client.POST('/api/v1/monitors/{id}/test', {params: {path: {id: args.id}}})) display(this, resp.data ?? resp, flags.output) } } diff --git a/src/commands/notification-policies/test.ts b/src/commands/notification-policies/test.ts index 3eaf186..1ef25c6 100644 --- a/src/commands/notification-policies/test.ts +++ b/src/commands/notification-policies/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class NotificationPoliciesTest extends Command { static description = 'Test a notification policy' @@ -11,7 +11,7 @@ export default class NotificationPoliciesTest extends Command { async run() { const {args, flags} = await this.parse(NotificationPoliciesTest) const client = buildClient(flags) - await typedPost(client, `/api/v1/notification-policies/${args.id}/test`) + await checkedFetch(client.POST('/api/v1/notification-policies/{id}/test', {params: {path: {id: args.id}}, body: {}})) this.log('Test dispatch sent.') } } diff --git a/src/commands/status.ts b/src/commands/status.ts index dfa79cb..f52dd7e 100644 --- a/src/commands/status.ts +++ b/src/commands/status.ts @@ -1,7 +1,10 @@ import {Command} from '@oclif/core' import {globalFlags, buildClient} from '../lib/base-command.js' -import {typedGet} from '../lib/typed-api.js' +import {apiGet} from '../lib/api-client.js' import {formatOutput, OutputFormat} from '../lib/output.js' +import type {components} from '../lib/api.generated.js' + +type DashboardOverviewDto = components['schemas']['DashboardOverviewDto'] export default class Status extends Command { static description = 'Show dashboard overview' @@ -11,7 +14,7 @@ export default class Status extends Command { async run() { const {flags} = await this.parse(Status) const client = buildClient(flags) - const resp = await typedGet<{data?: Record>}>(client, '/api/v1/dashboard/overview') + const resp = await apiGet<{data?: DashboardOverviewDto}>(client, '/api/v1/dashboard/overview') const overview = resp.data ?? {} const format = flags.output as OutputFormat diff --git a/src/commands/webhooks/test.ts b/src/commands/webhooks/test.ts index af03969..2474361 100644 --- a/src/commands/webhooks/test.ts +++ b/src/commands/webhooks/test.ts @@ -1,6 +1,6 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient} from '../../lib/base-command.js' -import {typedPost} from '../../lib/typed-api.js' +import {checkedFetch} from '../../lib/api-client.js' export default class WebhooksTest extends Command { static description = 'Send a test event to a webhook' @@ -11,7 +11,7 @@ export default class WebhooksTest extends Command { async run() { const {args, flags} = await this.parse(WebhooksTest) const client = buildClient(flags) - const resp = await typedPost<{data?: {success?: boolean}}>(client, `/api/v1/webhooks/${args.id}/test`) + const resp = await checkedFetch(client.POST('/api/v1/webhooks/{id}/test', {params: {path: {id: args.id}}})) this.log(resp.data?.success ? 'Test event delivered.' : 'Test delivery failed.') } } diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 6adc150..69a052d 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -85,3 +85,34 @@ export async function checkedFetch(promise: Promise<{data?: T; error?: unknow } return data as T } + +// ── 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 */ + +export function apiGet(client: ApiClient, path: string, params?: object): Promise { + return checkedFetch(client.GET(path as any, (params ? {params} : {}) as any)) +} + +export function apiPost(client: ApiClient, path: string, body: object): Promise { + return checkedFetch(client.POST(path as any, {body} as any)) +} + +export function apiPut(client: ApiClient, path: string, body: object): Promise { + return checkedFetch(client.PUT(path as any, {body} as any)) +} + +export function apiPatch(client: ApiClient, path: string, body: object): Promise { + return checkedFetch(client.PATCH(path as any, {body} as any)) +} + +export function apiDelete(client: ApiClient, path: string): Promise { + return checkedFetch(client.DELETE(path as any, {params: {path: {}}} as any)) +} + +/* eslint-enable @typescript-eslint/no-explicit-any */ diff --git a/src/lib/api.generated.ts b/src/lib/api.generated.ts index 73ba0fd..078e5c8 100644 --- a/src/lib/api.generated.ts +++ b/src/lib/api.generated.ts @@ -1427,6 +1427,30 @@ export interface paths { 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/api-keys": { parameters: { query?: never; @@ -2523,6 +2547,26 @@ export interface paths { 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/audit-log": { parameters: { query?: never; @@ -2644,36 +2688,50 @@ export interface paths { 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; + }; } export type webhooks = Record; export interface components { schemas: { - Actor: Record; - ApiKey: components["schemas"]["Actor"] & { - /** Format: int32 */ - orgId?: number; - /** Format: int32 */ - keyId?: number; - }; - Internal: components["schemas"]["Actor"]; - OrgContext: { - /** Format: int32 */ - id?: number; - /** @enum {string} */ - role?: "OWNER" | "ADMIN" | "MEMBER"; - }; - UI: components["schemas"]["Actor"] & { - userContext?: components["schemas"]["UserContext"]; - orgContext?: components["schemas"]["OrgContext"]; - /** Format: int32 */ - workspaceId?: number | null; - }; - UserContext: { - /** Format: int32 */ - id?: number; - /** @enum {string} */ - role?: "SUPERADMIN" | "ADMIN" | "USER"; - }; CreateSubscriptionRequest: { /** Format: int32 */ priceId?: number; @@ -3731,7 +3789,7 @@ export interface components { tags?: components["schemas"]["TagDto"][] | null; pingUrl?: string | null; environment?: components["schemas"]["Summary"]; - auth?: components["schemas"]["MonitorAuthDto"]; + auth?: components["schemas"]["ApiKeyAuthConfig"] | components["schemas"]["BasicAuthConfig"] | components["schemas"]["BearerAuthConfig"] | components["schemas"]["HeaderAuthConfig"]; incidentPolicy?: components["schemas"]["IncidentPolicyDto"]; alertChannelIds?: (string | null)[] | null; }; @@ -3888,6 +3946,7 @@ export interface components { createdAt: string; /** Format: date-time */ updatedAt: string; + configHash?: string | null; /** Format: date-time */ lastDeliveryAt?: string | null; lastDeliveryStatus?: string | null; @@ -4744,6 +4803,40 @@ export interface components { /** @description Whether this is the default environment for new monitors */ isDefault?: boolean; }; + /** @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. Defaults to 10. Max 60. + * @example 10 + */ + ttlMinutes?: number | null; + }; + /** @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; + }; + SingleValueResponseDeployLockDto: { + data?: components["schemas"]["DeployLockDto"]; + }; CreateApiKeyRequest: { /** @description Human-readable name to identify this API key */ name: string; @@ -5882,6 +5975,94 @@ export interface components { hasNext?: boolean; hasPrev?: boolean; }; + /** @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"]; + }; + /** @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; + }; + /** @description Organization the key belongs to */ + OrgInfo: { + /** + * Format: int32 + * @description Organization ID + */ + id?: number; + /** @description Organization name */ + name?: string; + }; + /** @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 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; + }; + SingleValueResponseAuthMeResponse: { + data?: components["schemas"]["AuthMeResponse"]; + }; AuditEventDto: { /** Format: int64 */ id?: number; @@ -5997,9 +6178,7 @@ export type $defs = Record; export interface operations { updateSubscription: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -6026,9 +6205,7 @@ export interface operations { }; updateOrgDetails: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -6054,9 +6231,7 @@ export interface operations { }; advanceStage: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6080,9 +6255,7 @@ export interface operations { }; me: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6102,9 +6275,7 @@ export interface operations { }; updateProfile: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6128,9 +6299,7 @@ export interface operations { }; getNotificationPreferences: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6150,9 +6319,7 @@ export interface operations { }; updateNotificationPreferences: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6176,9 +6343,7 @@ export interface operations { }; getWorkspace: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6200,9 +6365,7 @@ export interface operations { }; updateWorkspace: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6228,9 +6391,7 @@ export interface operations { }; deleteWorkspace: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6250,9 +6411,7 @@ export interface operations { }; updateUser: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { userId: number; @@ -6278,9 +6437,7 @@ export interface operations { }; updateOrganization: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -6306,9 +6463,7 @@ export interface operations { }; updateMemberRole: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -6333,9 +6488,7 @@ export interface operations { }; get: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6357,9 +6510,7 @@ export interface operations { }; update: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6385,9 +6536,7 @@ export interface operations { }; delete: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { workspaceId: number; @@ -6407,9 +6556,7 @@ export interface operations { }; get_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6431,9 +6578,7 @@ export interface operations { }; update_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6459,9 +6604,7 @@ export interface operations { }; delete_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6481,9 +6624,7 @@ export interface operations { }; update_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6509,9 +6650,7 @@ export interface operations { }; delete_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6531,9 +6670,7 @@ export interface operations { }; update_3: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { key: string; @@ -6559,9 +6696,7 @@ export interface operations { }; delete_3: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { key: string; @@ -6581,8 +6716,7 @@ export interface operations { }; get_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { includeMetrics?: boolean; }; header?: never; @@ -6606,9 +6740,7 @@ export interface operations { }; update_4: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6634,9 +6766,7 @@ export interface operations { }; delete_4: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6656,9 +6786,7 @@ export interface operations { }; get_3: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6678,9 +6806,7 @@ export interface operations { }; update_5: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6704,9 +6830,7 @@ export interface operations { }; markRead: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: number; @@ -6726,9 +6850,7 @@ export interface operations { }; markAllRead: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -6746,9 +6868,7 @@ export interface operations { }; getById: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6770,9 +6890,7 @@ export interface operations { }; update_6: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6798,9 +6916,7 @@ export interface operations { }; delete_5: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -6820,9 +6936,7 @@ export interface operations { }; get_4: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { /** @description Monitor UUID */ @@ -6854,9 +6968,7 @@ export interface operations { }; update_7: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { /** @description Monitor UUID */ @@ -6901,9 +7013,7 @@ export interface operations { }; update_8: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -6929,9 +7039,7 @@ export interface operations { }; set: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -6957,9 +7065,7 @@ export interface operations { }; remove: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -6979,9 +7085,7 @@ export interface operations { }; update_9: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -7008,9 +7112,7 @@ export interface operations { }; remove_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -7031,9 +7133,7 @@ export interface operations { }; setChannels: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -7059,9 +7159,7 @@ export interface operations { }; get_5: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7083,9 +7181,7 @@ export interface operations { }; update_10: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7111,9 +7207,7 @@ export interface operations { }; delete_6: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7133,9 +7227,7 @@ export interface operations { }; changeStatus: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { userId: number; @@ -7159,9 +7251,7 @@ export interface operations { }; changeRole: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { userId: number; @@ -7185,9 +7275,7 @@ export interface operations { }; getById_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7209,9 +7297,7 @@ export interface operations { }; update_11: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7237,9 +7323,7 @@ export interface operations { }; delete_7: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7259,9 +7343,7 @@ export interface operations { }; get_6: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { slug: string; @@ -7283,9 +7365,7 @@ export interface operations { }; update_12: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { slug: string; @@ -7311,9 +7391,7 @@ export interface operations { }; delete_8: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { slug: string; @@ -7333,9 +7411,7 @@ export interface operations { }; update_13: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7361,9 +7437,7 @@ export interface operations { }; delete_9: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -7657,8 +7731,7 @@ export interface operations { }; create_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { ifNotExists?: boolean; }; header?: never; @@ -7684,8 +7757,7 @@ export interface operations { }; list: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { limit?: number; }; header?: never; @@ -7709,9 +7781,7 @@ export interface operations { }; createSubscriptionTransaction: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -7737,9 +7807,7 @@ export interface operations { }; quickMonitor: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -7763,9 +7831,7 @@ export interface operations { }; completeSetup: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -7789,9 +7855,7 @@ export interface operations { }; analyzeUrl: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -7815,9 +7879,7 @@ export interface operations { }; accept: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -7866,7 +7928,6 @@ export interface operations { listWorkspaces: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -7890,9 +7951,7 @@ export interface operations { }; createWorkspace: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -7918,9 +7977,7 @@ export interface operations { }; listMembers: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -7942,9 +7999,7 @@ export interface operations { }; addMember: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -7970,9 +8025,7 @@ export interface operations { }; reEnableAdapter: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { serviceId: string; @@ -7995,7 +8048,6 @@ export interface operations { list_1: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -8017,9 +8069,7 @@ export interface operations { }; create_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8044,7 +8094,6 @@ export interface operations { list_2: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -8066,9 +8115,7 @@ export interface operations { }; create_3: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8092,9 +8139,7 @@ export interface operations { }; test: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8120,9 +8165,7 @@ export interface operations { }; rotateSigningSecret: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8142,9 +8185,7 @@ export interface operations { }; rotateDek: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8165,7 +8206,6 @@ export interface operations { list_3: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -8187,9 +8227,7 @@ export interface operations { }; create_4: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8213,9 +8251,7 @@ export interface operations { }; subscribe: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { slug: string; @@ -8241,9 +8277,7 @@ export interface operations { }; list_4: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8263,9 +8297,7 @@ export interface operations { }; create_5: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8289,9 +8321,7 @@ export interface operations { }; list_5: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8311,9 +8341,7 @@ export interface operations { }; create_6: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8337,9 +8365,7 @@ export interface operations { }; addMember_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8365,9 +8391,7 @@ export interface operations { }; list_6: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8387,9 +8411,7 @@ export interface operations { }; create_7: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8413,9 +8435,7 @@ export interface operations { }; test_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8441,9 +8461,7 @@ export interface operations { }; acknowledge: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8466,7 +8484,6 @@ export interface operations { list_7: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; /** @description Filter by enabled state */ enabled?: boolean; /** @description Filter by monitor type */ @@ -8500,9 +8517,7 @@ export interface operations { }; create_8: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8526,9 +8541,7 @@ export interface operations { }; add: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { monitorId: string; @@ -8554,9 +8567,7 @@ export interface operations { }; testExisting: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8578,9 +8589,7 @@ export interface operations { }; getMonitorTags: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8602,9 +8611,7 @@ export interface operations { }; addMonitorTags: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8630,9 +8637,7 @@ export interface operations { }; removeMonitorTags: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8656,9 +8661,7 @@ export interface operations { }; rotateToken: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8680,9 +8683,7 @@ export interface operations { }; resume: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8704,9 +8705,7 @@ export interface operations { }; pause: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -8728,9 +8727,7 @@ export interface operations { }; testAdHoc: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8754,9 +8751,7 @@ export interface operations { }; bulkAction: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8780,8 +8775,7 @@ export interface operations { }; list_8: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { /** @description Filter by monitor UUID */ monitorId?: string; /** @description Filter by status: 'active' or 'upcoming' */ @@ -8806,9 +8800,7 @@ export interface operations { }; create_9: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8832,9 +8824,7 @@ export interface operations { }; list_9: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8854,9 +8844,7 @@ export interface operations { }; create_10: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8880,9 +8868,7 @@ export interface operations { }; revoke: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { inviteId: number; @@ -8902,9 +8888,7 @@ export interface operations { }; resend: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { inviteId: number; @@ -8927,7 +8911,6 @@ export interface operations { list_10: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; params: components["schemas"]["IncidentFilterParams"]; }; header?: never; @@ -8949,9 +8932,7 @@ export interface operations { }; create_11: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -8975,9 +8956,7 @@ export interface operations { }; addUpdate: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -9003,9 +8982,7 @@ export interface operations { }; resolve: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -9087,9 +9064,7 @@ export interface operations { }; list_11: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9109,9 +9084,7 @@ export interface operations { }; create_12: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9133,11 +9106,58 @@ export interface operations { }; }; }; - list_12: { + current: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: never; + header?: { + "x-phelm-workspace-id"?: number; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["SingleValueResponseDeployLockDto"]; + }; + }; + }; + }; + acquire: { + parameters: { + query?: never; + header?: { + /** @description Target workspace ID (defaults to 1) */ + "x-phelm-workspace-id"?: number; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AcquireDeployLockRequest"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["SingleValueResponseDeployLockDto"]; + }; }; + }; + }; + list_12: { + parameters: { + query?: never; header?: never; path?: never; cookie?: never; @@ -9157,9 +9177,7 @@ export interface operations { }; create_13: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9183,9 +9201,7 @@ export interface operations { }; revoke_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: number; @@ -9207,9 +9223,7 @@ export interface operations { }; regenerate: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: number; @@ -9231,9 +9245,7 @@ export interface operations { }; retry: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -9256,7 +9268,6 @@ export interface operations { list_13: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -9278,9 +9289,7 @@ export interface operations { }; create_14: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9304,9 +9313,7 @@ export interface operations { }; test_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -9328,9 +9335,7 @@ export interface operations { }; testConfig: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9381,9 +9386,7 @@ export interface operations { }; updateAlertSensitivity: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -9409,9 +9412,7 @@ export interface operations { }; delete_10: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: number; @@ -9431,9 +9432,7 @@ export interface operations { }; update_14: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: number; @@ -9653,9 +9652,7 @@ export interface operations { }; listActive: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -9677,9 +9674,7 @@ export interface operations { }; cancel: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -9700,7 +9695,6 @@ export interface operations { getUpcomingCharge: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; priceId: number; }; header?: never; @@ -9724,9 +9718,7 @@ export interface operations { }; getManagementUrls: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -9748,9 +9740,7 @@ export interface operations { }; getCustomerAuthToken: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -9772,9 +9762,7 @@ export interface operations { }; getEntitlements: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -9797,7 +9785,6 @@ export interface operations { searchOrganizations: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; query: string; paginationParams: components["schemas"]["PaginationParams"]; }; @@ -9820,9 +9807,7 @@ export interface operations { }; myOrgs: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9842,9 +9827,7 @@ export interface operations { }; stream: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9865,7 +9848,6 @@ export interface operations { listUsers: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -9887,9 +9869,7 @@ export interface operations { }; getStats: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9910,7 +9890,6 @@ export interface operations { listOrgs: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -9932,9 +9911,7 @@ export interface operations { }; getAdapterHealth: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -9954,8 +9931,7 @@ export interface operations { }; listDeliveries: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { limit?: number; }; header?: never; @@ -9979,9 +9955,7 @@ export interface operations { }; getSigningSecretInfo: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -10271,9 +10245,7 @@ export interface operations { }; list_14: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -10293,9 +10265,7 @@ export interface operations { }; get_8: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10317,9 +10287,7 @@ export interface operations { }; unsubscribe: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10339,9 +10307,7 @@ export interface operations { }; getHealth: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10363,8 +10329,7 @@ export interface operations { }; list_15: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { unreadOnly?: boolean; page?: number; size?: number; @@ -10388,9 +10353,7 @@ export interface operations { }; unreadCount: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -10410,9 +10373,7 @@ export interface operations { }; listDispatches: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10435,7 +10396,6 @@ export interface operations { listByIncident: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; /** @description UUID of the incident to inspect */ incident_id: string; }; @@ -10458,9 +10418,7 @@ export interface operations { }; getById_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10483,7 +10441,6 @@ export interface operations { listVersions: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -10507,9 +10464,7 @@ export interface operations { }; getVersion: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10532,8 +10487,7 @@ export interface operations { }; getUptime: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { /** @description Time window for uptime calculation */ window?: "24h" | "7d" | "30d" | "90d"; }; @@ -10585,8 +10539,7 @@ export interface operations { }; getResults: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { /** @description Start of time range (ISO 8601, inclusive); defaults to 24 hours ago */ from?: string; /** @description End of time range (ISO 8601, inclusive); defaults to now */ @@ -10651,8 +10604,7 @@ export interface operations { }; getSummary: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { /** @description Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets */ chartWindow?: "24h" | "7d" | "30d" | "90d"; }; @@ -10705,7 +10657,6 @@ export interface operations { list_16: { parameters: { query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; pageable: components["schemas"]["Pageable"]; }; header?: never; @@ -10747,9 +10698,7 @@ export interface operations { }; get_9: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10771,9 +10720,7 @@ export interface operations { }; overview: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -10811,10 +10758,29 @@ export interface operations { }; }; }; + me_1: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["SingleValueResponseAuthMeResponse"]; + }; + }; + }; + }; list_18: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; + query?: { action?: string; actorId?: number; resourceType?: string; @@ -10842,9 +10808,7 @@ export interface operations { }; listAttempts: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10866,9 +10830,7 @@ export interface operations { }; listDeliveries_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10890,9 +10852,7 @@ export interface operations { }; delete_11: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -10912,9 +10872,7 @@ export interface operations { }; removeMember: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { orgId: number; @@ -10935,9 +10893,7 @@ export interface operations { }; removeMember_1: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { id: string; @@ -10958,9 +10914,7 @@ export interface operations { }; remove_2: { parameters: { - query: { - actor: components["schemas"]["ApiKey"] | components["schemas"]["Internal"] | components["schemas"]["UI"]; - }; + query?: never; header?: never; path: { userId: number; @@ -10978,4 +10932,46 @@ export interface operations { }; }; }; + release: { + parameters: { + query?: never; + header?: { + "x-phelm-workspace-id"?: number; + }; + path: { + lockId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forceRelease: { + parameters: { + query?: never; + header?: { + "x-phelm-workspace-id"?: number; + }; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; } diff --git a/src/lib/crud-commands.ts b/src/lib/crud-commands.ts index bff3cf5..1254087 100644 --- a/src/lib/crud-commands.ts +++ b/src/lib/crud-commands.ts @@ -1,6 +1,7 @@ import {Command, Args} from '@oclif/core' import {globalFlags, buildClient, display} from './base-command.js' -import {typedGet, typedPost, typedPut, typedDelete} from './typed-api.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. @@ -16,11 +17,7 @@ export interface ResourceConfig { createFlags?: Record updateFlags?: Record bodyBuilder?: (flags: Record) => object -} - -interface PaginatedResponse { - data?: T[] - hasNext?: boolean + updateBodyBuilder?: (flags: Record) => object } export function createListCommand(config: ResourceConfig) { @@ -32,8 +29,8 @@ export function createListCommand(config: ResourceConfig) { async run() { const {flags} = await this.parse(ListCmd) const client = buildClient(flags) - const resp = await typedGet>(client, config.apiPath) - display(this, resp.data ?? [], flags.output, config.columns) + const items = await fetchPaginated(client, config.apiPath) + display(this, items, flags.output, config.columns) } } @@ -52,7 +49,7 @@ export function createGetCommand(config: ResourceConfig) { const {args, flags} = await this.parse(GetCmd) const client = buildClient(flags) const id = args[idLabel] - const resp = await typedGet<{data?: T}>(client, `${config.apiPath}/${id}`) + const resp = await apiGet<{data?: T}>(client, `${config.apiPath}/${id}`) display(this, resp.data ?? resp, flags.output) } } @@ -72,7 +69,7 @@ export function createCreateCommand(config: ResourceConfig) { const client = buildClient(flags) const raw = extractResourceFlags(flags, Object.keys(resourceFlags)) const body = config.bodyBuilder ? config.bodyBuilder(raw) : raw - const resp = await typedPost<{data?: T}>(client, config.apiPath, body) + const resp = await apiPost<{data?: T}>(client, config.apiPath, body) display(this, resp.data ?? resp, flags.output) } } @@ -94,8 +91,9 @@ export function createUpdateCommand(config: ResourceConfig) { const client = buildClient(flags) const id = args[idLabel] const raw = extractResourceFlags(flags, Object.keys(resourceFlags)) - const body = config.bodyBuilder ? config.bodyBuilder(raw) : raw - const resp = await typedPut<{data?: T}>(client, `${config.apiPath}/${id}`, body) + const builder = config.updateBodyBuilder ?? config.bodyBuilder + const body = builder ? builder(raw) : raw + const resp = await apiPut<{data?: T}>(client, `${config.apiPath}/${id}`, body) display(this, resp.data ?? resp, flags.output) } } @@ -115,7 +113,7 @@ export function createDeleteCommand(config: ResourceConfig) { const {args, flags} = await this.parse(DeleteCmd) const client = buildClient(flags) const id = args[idLabel] - await typedDelete(client, `${config.apiPath}/${id}`) + await apiDelete(client, `${config.apiPath}/${id}`) this.log(`${config.name} '${id}' deleted.`) } } diff --git a/src/lib/resource-types.ts b/src/lib/resource-types.ts new file mode 100644 index 0000000..7914dd6 --- /dev/null +++ b/src/lib/resource-types.ts @@ -0,0 +1,94 @@ +/** + * Central registry mapping each resource key to its generated OpenAPI types. + * + * This single definition drives type safety across CRUD commands, the YAML + * engine, and the diff/apply layer. All DTO, create-request, and + * update-request types flow from here — no manual duplication. + */ +import type {components} from './api.generated.js' + +type Schemas = components['schemas'] + +export type ResourceKey = + | 'monitor' + | 'incident' + | 'alertChannel' + | 'notificationPolicy' + | 'environment' + | 'secret' + | 'tag' + | 'resourceGroup' + | 'webhook' + | 'apiKey' + | 'serviceSubscription' + +export interface ResourceTypeEntry< + TDto, + TCreate = never, + TUpdate = never, +> { + dto: TDto + create: TCreate + update: TUpdate +} + +export interface ResourceTypeMap { + monitor: ResourceTypeEntry< + Schemas['MonitorDto'], + Schemas['CreateMonitorRequest'], + Schemas['UpdateMonitorRequest'] + > + incident: ResourceTypeEntry< + Schemas['IncidentDto'], + Schemas['CreateManualIncidentRequest'] + > + alertChannel: ResourceTypeEntry< + Schemas['AlertChannelDto'], + Schemas['CreateAlertChannelRequest'], + Schemas['UpdateAlertChannelRequest'] + > + notificationPolicy: ResourceTypeEntry< + Schemas['NotificationPolicyDto'], + Schemas['CreateNotificationPolicyRequest'], + Schemas['UpdateNotificationPolicyRequest'] + > + environment: ResourceTypeEntry< + Schemas['EnvironmentDto'], + Schemas['CreateEnvironmentRequest'], + Schemas['UpdateEnvironmentRequest'] + > + secret: ResourceTypeEntry< + Schemas['SecretDto'], + Schemas['CreateSecretRequest'], + Schemas['UpdateSecretRequest'] + > + tag: ResourceTypeEntry< + Schemas['TagDto'], + Schemas['CreateTagRequest'], + Schemas['UpdateTagRequest'] + > + resourceGroup: ResourceTypeEntry< + Schemas['ResourceGroupDto'], + Schemas['CreateResourceGroupRequest'], + Schemas['UpdateResourceGroupRequest'] + > + webhook: ResourceTypeEntry< + Schemas['WebhookEndpointDto'], + Schemas['CreateWebhookEndpointRequest'], + Schemas['UpdateWebhookEndpointRequest'] + > + apiKey: ResourceTypeEntry< + Schemas['ApiKeyDto'], + Schemas['CreateApiKeyRequest'] + > + serviceSubscription: ResourceTypeEntry< + Schemas['ServiceSubscriptionDto'] + > +} + +/** Convenience: extract the DTO type for a given resource key. */ +export type DtoOf = ResourceTypeMap[K]['dto'] +/** Convenience: extract the create request type for a given resource key. */ +export type CreateOf = ResourceTypeMap[K]['create'] +/** Convenience: extract the update request type for a given resource key. */ +export type UpdateOf = ResourceTypeMap[K]['update'] diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 42234a4..deb18ce 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -31,6 +31,7 @@ type CreateMonitorRequest = Schemas['CreateMonitorRequest'] type CreateManualIncidentRequest = Schemas['CreateManualIncidentRequest'] type CreateAlertChannelRequest = Schemas['CreateAlertChannelRequest'] type CreateNotificationPolicyRequest = Schemas['CreateNotificationPolicyRequest'] +type UpdateNotificationPolicyRequest = Schemas['UpdateNotificationPolicyRequest'] type CreateApiKeyRequest = Schemas['CreateApiKeyRequest'] const MONITOR_TYPES: MonitorType[] = ['HTTP', 'DNS', 'TCP', 'ICMP', 'HEARTBEAT', 'MCP_SERVER'] @@ -39,13 +40,13 @@ const INCIDENT_SEVERITIES: IncidentSeverity[] = ['DOWN', 'DEGRADED', 'MAINTENANC 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', + SLACK: 'SlackChannelConfig', + EMAIL: 'EmailChannelConfig', + PAGERDUTY: 'PagerDutyChannelConfig', + OPSGENIE: 'OpsGenieChannelConfig', + DISCORD: 'DiscordChannelConfig', + TEAMS: 'TeamsChannelConfig', + WEBHOOK: 'WebhookChannelConfig', } // ── Resource definitions ─────────────────────────────────────────────── @@ -105,7 +106,7 @@ export const MONITORS: ResourceConfig = { if (raw.name !== undefined) body.name = raw.name if (raw.frequency) body.frequencySeconds = Number(raw.frequency) if (raw.url !== undefined || raw.method !== undefined) { - body.config = {monitorType: 'HTTP', url: raw.url, method: (raw.method as HttpMethod) || 'GET'} + body.config = {url: raw.url, method: (raw.method as HttpMethod) || 'GET'} } return body }, @@ -248,6 +249,16 @@ export const NOTIFICATION_POLICIES: ResourceConfig = { } return body }, + updateBodyBuilder: (raw) => { + const body: Partial = {} + if (raw.name !== undefined) body.name = String(raw.name) + if (raw.enabled !== undefined) body.enabled = raw.enabled as boolean + if (raw['channel-ids'] !== undefined) { + const channelIds = String(raw['channel-ids']).split(',').map((s) => s.trim()).filter(Boolean) + body.escalation = {steps: [{channelIds, delayMinutes: 0}]} + } + return body + }, } export const ENVIRONMENTS: ResourceConfig = { @@ -264,11 +275,9 @@ export const ENVIRONMENTS: ResourceConfig = { createFlags: { name: Flags.string({description: desc('CreateEnvironmentRequest', 'name'), required: true}), slug: Flags.string({description: desc('CreateEnvironmentRequest', 'slug'), required: true}), - color: Flags.string({description: desc('CreateTagRequest', 'color', 'Color hex code')}), }, updateFlags: { name: Flags.string({description: desc('UpdateEnvironmentRequest', 'name')}), - color: Flags.string({description: 'New color hex code'}), }, } @@ -388,6 +397,7 @@ export const DEPENDENCIES: ResourceConfig = { name: 'dependency', plural: 'dependencies', apiPath: '/api/v1/service-subscriptions', + idField: 'subscriptionId', columns: [ {header: 'ID', get: (r) => r.subscriptionId ?? ''}, {header: 'SERVICE', get: (r) => r.name ?? ''}, diff --git a/src/lib/typed-api.ts b/src/lib/typed-api.ts index 1b0266d..f640482 100644 --- a/src/lib/typed-api.ts +++ b/src/lib/typed-api.ts @@ -1,48 +1,11 @@ /** - * Typed API transport layer. + * Pagination helper for Spring Boot Pageable endpoints. * - * The OpenAPI spec includes a required `actor` query parameter on every - * operation — a Spring Security @AuthenticationPrincipal artefact. The - * server resolves the actor from the auth token; clients never send it. - * This forces `as any` casts when calling openapi-fetch methods. - * - * This module isolates those casts to ONE file while exposing fully-typed - * generic responses. All handler / resolver / applier code specifies the - * expected DTO type via the generic, achieving compile-time safety - * everywhere else. - * - * TODO(api): Add @Hidden to the @AuthenticationPrincipal parameter in API - * controllers, regenerate the OpenAPI spec, then remove these casts and - * call client.GET/POST/PUT/DELETE directly with full path-level types. + * Uses `apiGet` from api-client (which centralizes the dynamic-path cast) + * to iterate through pages until `hasNext` is false. */ import type {ApiClient} from './api-client.js' -import {checkedFetch} from './api-client.js' - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export function typedGet(client: ApiClient, path: string, query?: Record): Promise { - return checkedFetch(client.GET(path as any, query ? {params: {query} as any} : (undefined as any))) -} - -export function typedPost(client: ApiClient, path: string, body?: unknown): Promise { - return checkedFetch(client.POST(path as any, body !== undefined ? ({body} as any) : (undefined as any))) -} - -export function typedPut(client: ApiClient, path: string, body?: unknown): Promise { - return checkedFetch(client.PUT(path as any, body !== undefined ? ({body} as any) : (undefined as any))) -} - -export function typedPatch(client: ApiClient, path: string, body?: unknown): Promise { - return checkedFetch(client.PATCH(path as any, body !== undefined ? ({body} as any) : (undefined as any))) -} - -export function typedDelete(client: ApiClient, path: string): Promise { - return checkedFetch(client.DELETE(path as any, undefined as any)) -} - -/* eslint-enable @typescript-eslint/no-explicit-any */ - -// ── Pagination helper ─────────────────────────────────────────────────── +import {apiGet} from './api-client.js' interface PaginatedResponse { data?: T[] @@ -59,7 +22,7 @@ export async function fetchPaginated( let page = 0 while (true) { - const resp = await typedGet>(client, path, {page, size: API_PAGE_SIZE}) + const resp = await apiGet>(client, path, {query: {page, size: API_PAGE_SIZE}}) results.push(...(resp.data ?? [])) if (!resp.hasNext) break page++ diff --git a/src/lib/yaml/applier.ts b/src/lib/yaml/applier.ts index b15d07b..6aa1a50 100644 --- a/src/lib/yaml/applier.ts +++ b/src/lib/yaml/applier.ts @@ -5,7 +5,7 @@ * handlers in handlers.ts — no switch/case or `as YamlFoo` casts here. */ import type {ApiClient} from '../api-client.js' -import {typedPost, typedDelete} from '../typed-api.js' +import {checkedFetch, apiDelete} from '../api-client.js' import {HANDLER_MAP} from './handlers.js' import type {Changeset, Change, HandledResourceType} from './types.js' import type {ResolvedRefs} from './resolver.js' @@ -97,8 +97,7 @@ export async function apply( for (const change of changeset.deletes) { try { const handler = lookupHandler(change.resourceType, 'delete') - const path = handler.deletePath(change.existingId!) - await typedDelete(client, path) + await apiDelete(client, handler.deletePath(change.existingId!)) succeeded.push({action: 'delete', resourceType: change.resourceType, refKey: change.refKey}) } catch (err) { failed.push({ @@ -141,7 +140,7 @@ async function applyMembership(change: Change, refs: ResolvedRefs, client: ApiCl memberId = refs.require('dependencies', desired.memberRef) } - await typedPost(client, `/api/v1/resource-groups/${groupId}/members`, {memberType, memberId}) + await checkedFetch(client.POST('/api/v1/resource-groups/{id}/members', {params: {path: {id: groupId}}, body: {memberType, memberId}})) } function lookupHandler(resourceType: string, action: string) { diff --git a/src/lib/yaml/entitlements.ts b/src/lib/yaml/entitlements.ts index df78e96..a95d2cc 100644 --- a/src/lib/yaml/entitlements.ts +++ b/src/lib/yaml/entitlements.ts @@ -3,25 +3,11 @@ * planned resource creation against plan limits. */ import type {ApiClient} from '../api-client.js' -import {typedGet} from '../typed-api.js' +import {checkedFetch} from '../api-client.js' +import type {components} from '../api.generated.js' import type {Changeset} from './types.js' -interface Entitlement { - value: number -} - -interface AuthMePlan { - tier?: string - entitlements?: Record - usage?: Record - trialActive?: boolean - subscriptionStatus?: string -} - -interface AuthMeData { - plan?: AuthMePlan - organization?: {name?: string} -} +type AuthMeResponse = components['schemas']['AuthMeResponse'] export interface EntitlementWarning { resource: string @@ -56,10 +42,10 @@ export async function checkEntitlements( client: ApiClient, changeset: Changeset, ): Promise { - let data: AuthMeData + let data: AuthMeResponse try { - const resp = await typedGet(client, '/api/v1/auth/me') - data = narrowAuthMeData(resp) + const resp = await checkedFetch<{data?: AuthMeResponse}>(client.GET('/api/v1/auth/me')) + data = resp.data ?? {} } catch { return null } @@ -79,15 +65,16 @@ export async function checkEntitlements( for (const [entitlementKey, createsOfType] of createCounts) { const entitlement = plan.entitlements[entitlementKey] - if (!entitlement || entitlement.value >= UNLIMITED) continue + const limit = entitlement?.value + if (limit == null || limit >= UNLIMITED) continue const currentUsage = plan.usage[entitlementKey] ?? 0 - if (currentUsage + createsOfType > entitlement.value) { + if (currentUsage + createsOfType > limit) { warnings.push({ resource: entitlementKey, current: currentUsage, creating: createsOfType, - limit: entitlement.value, + limit, }) } } @@ -107,18 +94,6 @@ export async function checkEntitlements( return {plan: tier, warnings, header} } -function narrowAuthMeData(resp: unknown): AuthMeData { - if (!resp || typeof resp !== 'object') return {} - const obj = resp as Record - const inner = (obj.data && typeof obj.data === 'object' ? obj.data : resp) as Record - return { - plan: inner.plan && typeof inner.plan === 'object' ? inner.plan as AuthMePlan : undefined, - organization: inner.organization && typeof inner.organization === 'object' - ? inner.organization as AuthMeData['organization'] - : undefined, - } -} - export function formatEntitlementWarnings(warnings: EntitlementWarning[]): string { const lines = warnings.map((w) => ` ⚠ ${w.resource}: deploying ${w.creating} new but only ${w.limit - w.current} remaining (${w.current}/${w.limit} used)`, diff --git a/src/lib/yaml/handlers.ts b/src/lib/yaml/handlers.ts index c06eeb5..b9a1d4a 100644 --- a/src/lib/yaml/handlers.ts +++ b/src/lib/yaml/handlers.ts @@ -11,8 +11,16 @@ * - toCurrentSnapshot(api) → TSnapshot (what we HAVE) * - hasChanged = !isEqual(desired, current) * - * TypeScript enforces both functions return the same TSnapshot type. Adding a - * field to TSnapshot → compile error until both sides are updated. + * Snapshot types are derived from the OpenAPI-generated Update*Request schemas + * (via Required). This guarantees that when the API + * contract changes — a field is added, removed, or renamed — the TypeScript + * compiler immediately errors in the snapshot functions, preventing silent drift. + * + * Three resources use custom snapshot types because their update semantics + * don't map 1:1 to an UpdateXRequest schema: + * - secret: write-only value, compared by SHA-256 hash + * - alertChannel: complex config union, compared by content-addressed hash + * - dependency: no single update endpoint (split across two API calls) * * Adding a new resource type requires: * 1. Adding it to HandledResourceType in types.ts @@ -35,18 +43,14 @@ import { toCreateTagRequest, toCreateEnvironmentRequest, toCreateSecretRequest, toCreateAlertChannelRequest, toCreateNotificationPolicyRequest, toCreateWebhookRequest, toCreateResourceGroupRequest, - toCreateMonitorRequest, toUpdateMonitorRequest, + toCreateMonitorRequest, toUpdateMonitorRequest, toAuthConfig, + toCreateAssertionRequest, toIncidentPolicy, } from './transform.js' -import {typedPost, typedPut, typedPatch, fetchPaginated} from '../typed-api.js' +import {fetchPaginated} from '../typed-api.js' +import {checkedFetch, apiPatch} from '../api-client.js' type Schemas = components['schemas'] -// ── Response wrappers (match generated SingleValueResponse* pattern) ───── - -interface SingleValueResponse { - data?: T -} - // ── Public interface ──────────────────────────────────────────────────── /** @@ -82,8 +86,8 @@ export interface ResourceHandler { /** * Input shape for defineHandler. Handlers provide two snapshot functions - * that both return TSnapshot, OR set alwaysChanged for write-only resources. - * hasChanged is automatically derived — handlers never implement it manually. + * that both return TSnapshot. hasChanged is automatically derived from + * snapshot comparison — handlers never implement it manually. */ interface HandlerDef { readonly resourceType: HandledResourceType @@ -96,23 +100,8 @@ interface HandlerDef { getApiId(api: TApiDto): string getManagedBy?: (api: TApiDto) => string | undefined - /** - * true → resource always reports as changed (e.g. secrets: value is write-only, - * the API never returns it, so we can't compare). - */ - alwaysChanged?: boolean - - /** - * Project the YAML config + current API state into a comparable snapshot. - * For undefined (optional) YAML fields, use the current API value so they - * don't trigger a false diff. - */ - toDesiredSnapshot?: (yaml: TYaml, api: TApiDto, refs: ResolvedRefs) => TSnapshot - - /** - * Project the API DTO into the same comparable snapshot shape. - */ - toCurrentSnapshot?: (api: TApiDto) => TSnapshot + toDesiredSnapshot(yaml: TYaml, api: TApiDto, refs: ResolvedRefs): TSnapshot + toCurrentSnapshot(api: TApiDto): TSnapshot fetchAll(client: ApiClient): Promise applyCreate(yaml: TYaml, refs: ResolvedRefs, client: ApiClient): Promise @@ -125,7 +114,7 @@ interface HandlerDef { * derives hasChanged from snapshot comparison, then type-erases to * ResourceHandler (defaults) for registry storage. */ -function defineHandler( +function defineHandler( h: HandlerDef, ): ResourceHandler { const handler: ResourceHandler = { @@ -140,8 +129,6 @@ function defineHandler( getManagedBy: h.getManagedBy, hasChanged(yaml: TYaml, api: TApiDto, refs: ResolvedRefs): boolean { - if (h.alwaysChanged) return true - if (!h.toDesiredSnapshot || !h.toCurrentSnapshot) return true return !isEqual(h.toDesiredSnapshot(yaml, api, refs), h.toCurrentSnapshot(api)) }, @@ -163,7 +150,7 @@ function sortedIds(ids: string[]): string[] { return [...ids].sort() } -function sha256Hex(input: string): string { +export function sha256Hex(input: string): string { return createHash('sha256').update(input, 'utf8').digest('hex') } @@ -172,7 +159,7 @@ function sha256Hex(input: string): string { * nesting level. Produces the same output regardless of JS engine key * insertion order, matching the Java-side TreeMap-based canonical JSON. */ -function stableStringify(obj: unknown): string { +export function stableStringify(obj: unknown): string { if (obj === null || obj === undefined) return 'null' if (typeof obj !== 'object') return JSON.stringify(obj) if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']' @@ -183,10 +170,7 @@ function stableStringify(obj: unknown): string { // ── Tag ───────────────────────────────────────────────────────────────── -interface TagSnapshot { - name: string - color: string | null -} +type TagSnapshot = Required const tagHandler = defineHandler({ resourceType: 'tag', @@ -203,31 +187,25 @@ const tagHandler = defineHandler({ color: yaml.color ?? api.color ?? null, }), toCurrentSnapshot: (api) => ({ - name: api.name ?? '', + name: api.name ?? null, color: api.color ?? null, }), fetchAll: (client) => fetchPaginated(client, '/api/v1/tags'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, '/api/v1/tags', toCreateTagRequest(yaml), - ) + const resp = await checkedFetch(client.POST('/api/v1/tags', {body: toCreateTagRequest(yaml)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, _refs, client) { - await typedPut(client, `/api/v1/tags/${id}`, toCreateTagRequest(yaml)) + await checkedFetch(client.PUT('/api/v1/tags/{id}', {params: {path: {id}}, body: toCreateTagRequest(yaml)})) }, deletePath: (id) => `/api/v1/tags/${id}`, }) // ── Environment ───────────────────────────────────────────────────────── -interface EnvironmentSnapshot { - name: string - isDefault: boolean - variables: Record | null -} +type EnvironmentSnapshot = Required const environmentHandler = defineHandler({ resourceType: 'environment', @@ -241,37 +219,34 @@ const environmentHandler = defineHandler ({ name: yaml.name, - isDefault: yaml.isDefault ?? api.isDefault ?? false, + isDefault: yaml.isDefault ?? api.isDefault ?? null, variables: yaml.variables ?? api.variables ?? null, }), toCurrentSnapshot: (api) => ({ - name: api.name ?? '', - isDefault: api.isDefault ?? false, + name: api.name ?? null, + isDefault: api.isDefault ?? null, variables: api.variables ?? null, }), fetchAll: (client) => fetchPaginated(client, '/api/v1/environments'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, '/api/v1/environments', toCreateEnvironmentRequest(yaml), - ) + const resp = await checkedFetch(client.POST('/api/v1/environments', {body: toCreateEnvironmentRequest(yaml)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, _refs, client) { - await typedPut(client, `/api/v1/environments/${id}`, { + await checkedFetch(client.PUT('/api/v1/environments/{slug}', {params: {path: {slug: id}}, body: { name: yaml.name, variables: yaml.variables ?? null, isDefault: yaml.isDefault, - }) + }})) }, deletePath: (id) => `/api/v1/environments/${id}`, }) // ── Secret ────────────────────────────────────────────────────────────── -interface SecretSnapshot { - key: string - valueHash: string -} +// Custom snapshot: the API never returns the plaintext value (write-only), +// so we compare by SHA-256 hash instead of using UpdateSecretRequest. +type SecretSnapshot = { key: string; valueHash: string } const secretHandler = defineHandler({ resourceType: 'secret', @@ -295,24 +270,20 @@ const secretHandler = defineHandler fetchPaginated(client, '/api/v1/secrets'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, '/api/v1/secrets', toCreateSecretRequest(yaml), - ) + const resp = await checkedFetch(client.POST('/api/v1/secrets', {body: toCreateSecretRequest(yaml)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, _id, _refs, client) { - await typedPut(client, `/api/v1/secrets/${yaml.key}`, {value: yaml.value}) + await checkedFetch(client.PUT('/api/v1/secrets/{key}', {params: {path: {key: yaml.key}}, body: {value: yaml.value}})) }, deletePath: (id) => `/api/v1/secrets/${id}`, }) // ── Alert Channel ─────────────────────────────────────────────────────── -interface AlertChannelSnapshot { - name: string - channelType: string - configHash: string -} +// Custom snapshot: config is a complex discriminated union, compared by +// content-addressed SHA-256 hash (matching the API's configHash field). +type AlertChannelSnapshot = { name: string; channelType: string; configHash: string } const alertChannelHandler = defineHandler({ resourceType: 'alertChannel', @@ -335,34 +306,24 @@ const alertChannelHandler = defineHandler ({ name: api.name, channelType: api.channelType?.toLowerCase?.() ?? '', - // configHash is available once the API is deployed with V89 migration. - // Pre-migration responses lack it → empty string → forces update (backfills hash). - configHash: (api as Record).configHash as string ?? '', + configHash: api.configHash ?? '', }), fetchAll: (client) => fetchPaginated(client, '/api/v1/alert-channels'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, '/api/v1/alert-channels', toCreateAlertChannelRequest(yaml), - ) + const resp = await checkedFetch(client.POST('/api/v1/alert-channels', {body: toCreateAlertChannelRequest(yaml)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, _refs, client) { - await typedPut(client, `/api/v1/alert-channels/${id}`, toCreateAlertChannelRequest(yaml)) + await checkedFetch(client.PUT('/api/v1/alert-channels/{id}', {params: {path: {id}}, body: toCreateAlertChannelRequest(yaml)})) }, deletePath: (id) => `/api/v1/alert-channels/${id}`, }) // ── Notification Policy ───────────────────────────────────────────────── -interface NotificationPolicySnapshot { - name: string - enabled: boolean - priority: number - matchRules: unknown - escalation: unknown -} +type NotificationPolicySnapshot = Required const notificationPolicyHandler = defineHandler({ resourceType: 'notificationPolicy', @@ -380,7 +341,7 @@ const notificationPolicyHandler = defineHandler fetchPaginated(client, '/api/v1/notification-policies'), async applyCreate(yaml, refs, client) { - const resp = await typedPost>( - client, '/api/v1/notification-policies', toCreateNotificationPolicyRequest(yaml, refs), - ) + const resp = await checkedFetch(client.POST('/api/v1/notification-policies', {body: toCreateNotificationPolicyRequest(yaml, refs)})) return resp.data?.id != null ? String(resp.data.id) : undefined }, async applyUpdate(yaml, id, refs, client) { - await typedPut(client, `/api/v1/notification-policies/${id}`, toCreateNotificationPolicyRequest(yaml, refs)) + await checkedFetch(client.PUT('/api/v1/notification-policies/{id}', {params: {path: {id}}, body: toCreateNotificationPolicyRequest(yaml, refs)})) }, deletePath: (id) => `/api/v1/notification-policies/${id}`, }) // ── Webhook ───────────────────────────────────────────────────────────── -interface WebhookSnapshot { - url: string - description: string | null - subscribedEvents: string[] -} +type WebhookSnapshot = Required const webhookHandler = defineHandler({ resourceType: 'webhook', @@ -428,43 +383,34 @@ const webhookHandler = defineHandler ({ - url: api.url ?? '', + url: api.url ?? null, description: api.description ?? null, - subscribedEvents: sortedIds(api.subscribedEvents ?? []), + subscribedEvents: api.subscribedEvents ? sortedIds(api.subscribedEvents) : null, + enabled: api.enabled ?? null, }), fetchAll: (client) => fetchPaginated(client, '/api/v1/webhooks'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, '/api/v1/webhooks', toCreateWebhookRequest(yaml), - ) + const resp = await checkedFetch(client.POST('/api/v1/webhooks', {body: toCreateWebhookRequest(yaml)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, _refs, client) { - await typedPut(client, `/api/v1/webhooks/${id}`, toCreateWebhookRequest(yaml)) + await checkedFetch(client.PUT('/api/v1/webhooks/{id}', {params: {path: {id}}, body: toCreateWebhookRequest(yaml)})) }, deletePath: (id) => `/api/v1/webhooks/${id}`, }) // ── Resource Group ────────────────────────────────────────────────────── -interface ResourceGroupSnapshot { - name: string - description: string | null - alertPolicyId: string | null - defaultFrequency: number | null - defaultRegions: string[] | null - defaultRetryStrategy: unknown - defaultAlertChannelIds: string[] | null - defaultEnvironmentId: string | null - healthThresholdType: string | null - healthThresholdValue: number | null - suppressMemberAlerts: boolean | undefined - confirmationDelaySeconds: number | null - recoveryCooldownMinutes: number | null +// defaultRetryStrategy is optional (not nullable) in the Update schema, +// but a group can legitimately have none, so we add | null. +type ResourceGroupSnapshotBase = Required +type ResourceGroupSnapshot = Omit & { + defaultRetryStrategy: ResourceGroupSnapshotBase['defaultRetryStrategy'] | null } const resourceGroupHandler = defineHandler({ @@ -488,7 +434,7 @@ const resourceGroupHandler = defineHandler refs.resolve('alertChannels', n) ?? n)) : (api.defaultAlertChannels ? sortedIds(nonNullStrings(api.defaultAlertChannels)) : null), defaultEnvironmentId: yaml.defaultEnvironment !== undefined @@ -496,7 +442,7 @@ const resourceGroupHandler = defineHandler fetchPaginated(client, '/api/v1/resource-groups'), async applyCreate(yaml, refs, client) { - const resp = await typedPost>( - client, '/api/v1/resource-groups', toCreateResourceGroupRequest(yaml, refs), - ) + const resp = await checkedFetch(client.POST('/api/v1/resource-groups', {body: toCreateResourceGroupRequest(yaml, refs)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, refs, client) { - await typedPut(client, `/api/v1/resource-groups/${id}`, toCreateResourceGroupRequest(yaml, refs)) + await checkedFetch(client.PUT('/api/v1/resource-groups/{id}', {params: {path: {id}}, body: toCreateResourceGroupRequest(yaml, refs)})) }, deletePath: (id) => `/api/v1/resource-groups/${id}`, }) // ── Monitor ───────────────────────────────────────────────────────────── -interface MonitorSnapshot { - name: string - type: string - config: unknown - enabled: boolean | undefined - frequencySeconds: number | undefined - regions: string[] | null - environmentId: string | null - tagIds: string[] | null - alertChannelIds: string[] | null - auth: unknown - assertions: unknown - incidentPolicy: unknown +// Derived from UpdateMonitorRequest minus control-only fields (clearAuth, +// clearEnvironmentId, managedBy) that are mutation signals, not state. +// auth and incidentPolicy need | null because monitors can lack them. +type MonitorSnapshotBase = Required> +type MonitorSnapshot = Omit & { + auth: MonitorSnapshotBase['auth'] | null + incidentPolicy: MonitorSnapshotBase['incidentPolicy'] | null } const monitorHandler = defineHandler({ @@ -560,154 +498,106 @@ const monitorHandler = defineHandler ({ name: yaml.name, - type: yaml.type, - config: yaml.config, - enabled: yaml.enabled ?? api.enabled, - frequencySeconds: yaml.frequency ?? api.frequencySeconds, + config: yaml.config as MonitorSnapshot['config'], + frequencySeconds: yaml.frequency ?? api.frequencySeconds ?? null, + enabled: yaml.enabled ?? api.enabled ?? null, regions: yaml.regions !== undefined ? sortedIds(yaml.regions) : (api.regions ? sortedIds(api.regions) : null), environmentId: yaml.environment !== undefined ? (refs.resolve('environments', yaml.environment) ?? null) : (api.environment?.id ?? null), - tagIds: yaml.tags !== undefined - ? sortedIds(yaml.tags.map((n) => refs.resolve('tags', n) ?? n)) - : extractTagIds(api), + assertions: yaml.assertions !== undefined + ? sortAssertions(yaml.assertions.map(toCreateAssertionRequest)) + : apiAssertionsToSnapshot(api.assertions), + auth: yaml.auth !== undefined + ? (toAuthConfig(yaml.auth, refs) ?? null) + : (api.auth ?? null), + incidentPolicy: yaml.incidentPolicy !== undefined + ? toIncidentPolicy(yaml.incidentPolicy) + : apiIncidentPolicyToSnapshot(api.incidentPolicy), alertChannelIds: yaml.alertChannels !== undefined ? sortedIds(yaml.alertChannels.map((n) => refs.resolve('alertChannels', n) ?? n)) : (api.alertChannelIds ? sortedIds(nonNullStrings(api.alertChannelIds)) : null), - auth: yaml.auth !== undefined - ? normalizeYamlAuth(yaml.auth, refs) - : normalizeApiAuth(api.auth), - assertions: yaml.assertions !== undefined - ? normalizeYamlAssertions(yaml.assertions) - : normalizeApiAssertions(api.assertions), - incidentPolicy: yaml.incidentPolicy !== undefined - ? normalizeIncidentPolicy(yaml.incidentPolicy) - : normalizeApiIncidentPolicy(api.incidentPolicy), + tags: yaml.tags !== undefined + ? { + tagIds: sortedIds(yaml.tags.map((n) => refs.resolve('tags', n)).filter((id): id is string => id !== undefined)), + newTags: yaml.tags.filter((n) => !refs.resolve('tags', n)).map((n) => ({name: n})), + } + : apiTagsToSnapshot(api), }), toCurrentSnapshot: (api) => ({ - name: api.name ?? '', - type: api.type ?? '', - config: api.config, - enabled: api.enabled, - frequencySeconds: api.frequencySeconds, + name: api.name ?? null, + config: api.config as MonitorSnapshot['config'], + frequencySeconds: api.frequencySeconds ?? null, + enabled: api.enabled ?? null, regions: api.regions ? sortedIds(api.regions) : null, environmentId: api.environment?.id ?? null, - tagIds: extractTagIds(api), + assertions: apiAssertionsToSnapshot(api.assertions), + auth: api.auth ?? null, + incidentPolicy: apiIncidentPolicyToSnapshot(api.incidentPolicy), alertChannelIds: api.alertChannelIds ? sortedIds(nonNullStrings(api.alertChannelIds)) : null, - auth: normalizeApiAuth(api.auth), - assertions: normalizeApiAssertions(api.assertions), - incidentPolicy: normalizeApiIncidentPolicy(api.incidentPolicy), + tags: apiTagsToSnapshot(api), }), fetchAll: (client) => fetchPaginated(client, '/api/v1/monitors'), async applyCreate(yaml, refs, client) { - const resp = await typedPost>( - client, '/api/v1/monitors', toCreateMonitorRequest(yaml, refs), - ) + const resp = await checkedFetch(client.POST('/api/v1/monitors', {body: toCreateMonitorRequest(yaml, refs)})) return resp.data?.id ?? undefined }, async applyUpdate(yaml, id, refs, client) { - await typedPut(client, `/api/v1/monitors/${id}`, toUpdateMonitorRequest(yaml, refs)) + await checkedFetch(client.PUT('/api/v1/monitors/{id}', {params: {path: {id}}, body: toUpdateMonitorRequest(yaml, refs)})) }, deletePath: (id) => `/api/v1/monitors/${id}`, }) -// ── Monitor snapshot normalization helpers ─────────────────────────────── - -function extractTagIds(api: Schemas['MonitorDto']): string[] | null { - if (!api.tags) return null - return sortedIds(api.tags.map((t) => String(t.id ?? '')).filter(Boolean)) -} - -interface NormalizedAuth { - type: string - secretId: string | null - headerName?: string -} +// ── Monitor snapshot helpers ───────────────────────────────────────────── -const AUTH_TYPE_CANONICAL: Record = { - BearerAuthConfig: 'bearer', - BasicAuthConfig: 'basic', - ApiKeyAuthConfig: 'api_key', - HeaderAuthConfig: 'header', - bearer: 'bearer', - basic: 'basic', - api_key: 'api_key', - header: 'header', +function sortAssertions( + assertions: Schemas['CreateAssertionRequest'][], +): Schemas['CreateAssertionRequest'][] { + return [...assertions].sort((a, b) => { + const aType = (a.config as {type: string}).type + const bType = (b.config as {type: string}).type + return aType.localeCompare(bType) + }) } -function normalizeYamlAuth(auth: YamlMonitor['auth'], refs: ResolvedRefs): NormalizedAuth | null { - if (!auth) return null - const base: NormalizedAuth = { - type: AUTH_TYPE_CANONICAL[auth.type] ?? auth.type, - secretId: refs.resolve('secrets', auth.secret) ?? null, - } - if ('headerName' in auth) base.headerName = auth.headerName - return base -} - -function normalizeApiAuth(auth: Schemas['MonitorDto']['auth']): NormalizedAuth | null { - if (!auth) return null - const config = auth.config as Record | undefined - const base: NormalizedAuth = { - type: AUTH_TYPE_CANONICAL[auth.authType ?? ''] ?? (auth.authType ?? ''), - secretId: (config?.vaultSecretId as string | null) ?? null, - } - if (config?.headerName) base.headerName = config.headerName as string - return base -} - -interface NormalizedAssertion { - type: string - config: Record - severity: string -} - -function normalizeYamlAssertions(assertions: YamlMonitor['assertions']): NormalizedAssertion[] | null { +function apiAssertionsToSnapshot( + assertions: Schemas['MonitorDto']['assertions'], +): Schemas['CreateAssertionRequest'][] | null { if (!assertions) return null - return assertions - .map((a) => ({type: a.type, config: a.config ?? {}, severity: a.severity ?? 'fail'})) - .sort((a, b) => a.type.localeCompare(b.type)) + return sortAssertions(assertions.map((a) => ({ + config: a.config as Schemas['CreateAssertionRequest']['config'], + severity: a.severity, + }))) } -function normalizeApiAssertions(assertions: Schemas['MonitorDto']['assertions']): NormalizedAssertion[] | null { - if (!assertions) return null - return assertions - .map((a) => { - const config = (a.config ?? {}) as Record - const {type, ...rest} = config - return {type: type as string, config: rest, severity: a.severity ?? 'fail'} - }) - .sort((a, b) => a.type.localeCompare(b.type)) -} - -function normalizeIncidentPolicy(policy: YamlMonitor['incidentPolicy']): unknown { +function apiIncidentPolicyToSnapshot( + policy: Schemas['MonitorDto']['incidentPolicy'], +): Schemas['UpdateIncidentPolicyRequest'] | null { if (!policy) return null return { - triggerRules: policy.triggerRules, - confirmation: policy.confirmation, - recovery: policy.recovery, + triggerRules: policy.triggerRules ?? [], + confirmation: policy.confirmation ?? {type: 'multi_region'}, + recovery: policy.recovery ?? {consecutiveSuccesses: 1, minRegionsPassing: 1, cooldownMinutes: 0}, } } -function normalizeApiIncidentPolicy(policy: Schemas['MonitorDto']['incidentPolicy']): unknown { - if (!policy) return null +function apiTagsToSnapshot(api: Schemas['MonitorDto']): Schemas['AddMonitorTagsRequest'] { + if (!api.tags) return {tagIds: null, newTags: []} return { - triggerRules: policy.triggerRules, - confirmation: policy.confirmation, - recovery: policy.recovery, + tagIds: sortedIds(api.tags.map((t) => String(t.id ?? '')).filter(Boolean)), + newTags: [], } } // ── Dependency ────────────────────────────────────────────────────────── -interface DependencySnapshot { - alertSensitivity: string | null - component: string | null -} +// Custom snapshot: there is no single UpdateDependencyRequest — updates are +// split across UpdateAlertSensitivityRequest and a generic PATCH. +type DependencySnapshot = { alertSensitivity: string | null; component: string | null } const dependencyHandler = defineHandler({ resourceType: 'dependency', @@ -731,24 +621,24 @@ const dependencyHandler = defineHandler fetchPaginated(client, '/api/v1/service-subscriptions'), async applyCreate(yaml, _refs, client) { - const resp = await typedPost>( - client, `/api/v1/service-subscriptions/${yaml.service}`, { + const resp = await checkedFetch(client.POST('/api/v1/service-subscriptions/{slug}', { + params: {path: {slug: yaml.service}}, + body: { alertSensitivity: yaml.alertSensitivity ?? null, componentId: yaml.component ?? null, }, - ) + })) return resp.data?.subscriptionId ?? undefined }, async applyUpdate(yaml, id, _refs, client) { if (yaml.alertSensitivity !== undefined) { - await typedPatch(client, `/api/v1/service-subscriptions/${id}/alert-sensitivity`, { - alertSensitivity: yaml.alertSensitivity, - }) + await checkedFetch(client.PATCH('/api/v1/service-subscriptions/{id}/alert-sensitivity', { + params: {path: {id}}, + body: {alertSensitivity: yaml.alertSensitivity}, + })) } if (yaml.component !== undefined) { - await typedPatch(client, `/api/v1/service-subscriptions/${id}`, { - componentId: yaml.component, - }) + await apiPatch(client, `/api/v1/service-subscriptions/${id}`, {componentId: yaml.component}) } }, deletePath: (id) => `/api/v1/service-subscriptions/${id}`, diff --git a/src/lib/yaml/resolver.ts b/src/lib/yaml/resolver.ts index e29db1b..386bed1 100644 --- a/src/lib/yaml/resolver.ts +++ b/src/lib/yaml/resolver.ts @@ -3,27 +3,27 @@ * handler methods and builds name/slug → UUID maps for YAML reference resolution. */ import type {ApiClient} from '../api-client.js' -import type {RefType} from './types.js' +import type {RefType, RefTypeDtoMap} from './types.js' import {allHandlers} from './handlers.js' export type {RefType} -interface RefEntry { +export interface RefEntry { id: string refKey: string managedBy?: string - raw: Record + raw: K extends keyof RefTypeDtoMap ? RefTypeDtoMap[K] : unknown } export class ResolvedRefs { private maps = new Map>() - get(type: RefType, refKey: string): RefEntry | undefined { - return this.maps.get(type)?.get(refKey) + get(type: K, refKey: string): RefEntry | undefined { + return this.maps.get(type)?.get(refKey) as RefEntry | undefined } resolve(type: RefType, refKey: string): string | undefined { - return this.get(type, refKey)?.id + return this.maps.get(type)?.get(refKey)?.id } require(type: RefType, refKey: string): string { @@ -34,16 +34,16 @@ export class ResolvedRefs { return id } - set(type: RefType, refKey: string, entry: RefEntry): void { + set(type: K, refKey: string, entry: RefEntry): void { if (!this.maps.has(type)) this.maps.set(type, new Map()) - this.maps.get(type)!.set(refKey, entry) + this.maps.get(type)!.set(refKey, entry as RefEntry) } - all(type: RefType): Map { - return this.maps.get(type) ?? new Map() + all(type: K): Map> { + return (this.maps.get(type) ?? new Map()) as Map> } - allEntries(type: RefType): RefEntry[] { + allEntries(type: K): RefEntry[] { return [...this.all(type).values()] } } @@ -62,11 +62,14 @@ export async function fetchAllRefs(client: ApiClient): Promise { const handler = handlers[i] for (const item of results[i]) { const refKey = handler.getApiRefKey(item) + // Handler.fetchAll() returns the correct DTO for its refType but TS + // can't narrow the correlation. The cast is safe — each handler's + // fetchAll returns exactly RefTypeDtoMap[handler.refType]. refs.set(handler.refType, refKey, { id: handler.getApiId(item), refKey, managedBy: handler.getManagedBy?.(item), - raw: item as Record, + raw: item as RefEntry['raw'], }) } } diff --git a/src/lib/yaml/transform.ts b/src/lib/yaml/transform.ts index 992f8dc..849150f 100644 --- a/src/lib/yaml/transform.ts +++ b/src/lib/yaml/transform.ts @@ -188,12 +188,12 @@ export function toUpdateMonitorRequest( } } -function toCreateAssertionRequest(a: YamlAssertion): Schemas['CreateAssertionRequest'] { +export function toCreateAssertionRequest(a: YamlAssertion): Schemas['CreateAssertionRequest'] { const config = {type: a.type, ...(a.config ?? {})} as Schemas['CreateAssertionRequest']['config'] return {config, severity: a.severity} } -function toAuthConfig(auth: YamlAuth, refs: ResolvedRefs): Schemas['CreateMonitorRequest']['auth'] { +export function toAuthConfig(auth: YamlAuth, refs: ResolvedRefs): Schemas['CreateMonitorRequest']['auth'] { const secretId = refs.resolve('secrets', auth.secret) ?? undefined switch (auth.type) { case 'BearerAuthConfig': @@ -207,7 +207,7 @@ function toAuthConfig(auth: YamlAuth, refs: ResolvedRefs): Schemas['CreateMonito } } -function toIncidentPolicy(policy: YamlIncidentPolicy): Schemas['UpdateIncidentPolicyRequest'] { +export function toIncidentPolicy(policy: YamlIncidentPolicy): Schemas['UpdateIncidentPolicyRequest'] { return { triggerRules: policy.triggerRules.map((r) => ({ type: r.type, diff --git a/src/lib/yaml/types.ts b/src/lib/yaml/types.ts index 5218d08..fa9e09f 100644 --- a/src/lib/yaml/types.ts +++ b/src/lib/yaml/types.ts @@ -4,6 +4,9 @@ * Extracted into a standalone module to avoid circular dependencies * between handlers, differ, resolver, and applier. */ +import type {components} from '../api.generated.js' + +type Schemas = components['schemas'] export type ChangeAction = 'create' | 'update' | 'delete' @@ -20,6 +23,19 @@ export type RefType = | 'notificationPolicies' | 'webhooks' | 'resourceGroups' | 'monitors' | 'dependencies' +/** Maps each RefType to the API DTO stored in RefEntry.raw. */ +export interface RefTypeDtoMap { + tags: Schemas['TagDto'] + environments: Schemas['EnvironmentDto'] + secrets: Schemas['SecretDto'] + alertChannels: Schemas['AlertChannelDto'] + notificationPolicies: Schemas['NotificationPolicyDto'] + webhooks: Schemas['WebhookEndpointDto'] + resourceGroups: Schemas['ResourceGroupDto'] + monitors: Schemas['MonitorDto'] + dependencies: Schemas['ServiceSubscriptionDto'] +} + export interface Change { action: ChangeAction resourceType: ResourceType diff --git a/test/yaml/applier.test.ts b/test/yaml/applier.test.ts index 5999a84..3e7a729 100644 --- a/test/yaml/applier.test.ts +++ b/test/yaml/applier.test.ts @@ -3,21 +3,14 @@ import {apply} from '../../src/lib/yaml/applier.js' import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' import type {Changeset, Change} from '../../src/lib/yaml/differ.js' -vi.mock('../../src/lib/typed-api.js', () => ({ - typedGet: vi.fn(), - typedPost: vi.fn(), - typedPut: vi.fn(), - typedPatch: vi.fn(), - typedDelete: vi.fn(), - fetchPaginated: vi.fn(), +vi.mock('../../src/lib/api-client.js', () => ({ + checkedFetch: vi.fn(async (p: unknown) => p), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiDelete: vi.fn(async (client: any, path: string) => client.DELETE(path, {params: {path: {}}})), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiPatch: vi.fn(async (client: any, path: string, body: object) => client.PATCH(path, {body})), })) -import {typedPost, typedPut, typedPatch, typedDelete} from '../../src/lib/typed-api.js' -const mockPost = vi.mocked(typedPost) -const mockPut = vi.mocked(typedPut) -const mockPatch = vi.mocked(typedPatch) -const mockDelete = vi.mocked(typedDelete) - function emptyChangeset(): Changeset { return {creates: [], updates: [], deletes: [], memberships: []} } @@ -26,11 +19,30 @@ function emptyRefs(): ResolvedRefs { return new ResolvedRefs() } -const fakeClient = {} as Parameters[2] +function makeFakeClient() { + return { + GET: vi.fn(), + POST: vi.fn(), + PUT: vi.fn(), + PATCH: vi.fn(), + DELETE: vi.fn(), + } as Parameters[2] +} describe('applier', () => { + let fakeClient: ReturnType + let mockPost: ReturnType + let mockPut: ReturnType + let mockPatch: ReturnType + let mockDelete: ReturnType + beforeEach(() => { vi.clearAllMocks() + fakeClient = makeFakeClient() + mockPost = fakeClient.POST as ReturnType + mockPut = fakeClient.PUT as ReturnType + mockPatch = fakeClient.PATCH as ReturnType + mockDelete = fakeClient.DELETE as ReturnType }) describe('creates', () => { @@ -44,7 +56,7 @@ describe('applier', () => { expect(result.succeeded).toHaveLength(1) expect(result.succeeded[0].id).toBe('tag-new') expect(result.failed).toHaveLength(0) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/tags', {name: 'prod', color: '#FF0000'}) + expect(mockPost).toHaveBeenCalledWith('/api/v1/tags', {body: {name: 'prod', color: '#FF0000'}}) }) it('creates an environment', async () => { @@ -55,7 +67,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/environments', expect.objectContaining({name: 'Staging', slug: 'staging'})) + expect(mockPost).toHaveBeenCalledWith('/api/v1/environments', {body: expect.objectContaining({name: 'Staging', slug: 'staging'})}) }) it('creates a secret', async () => { @@ -66,7 +78,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets', {key: 'api-key', value: 'secret123'}) + expect(mockPost).toHaveBeenCalledWith('/api/v1/secrets', {body: {key: 'api-key', value: 'secret123'}}) }) it('creates an alert channel', async () => { @@ -77,7 +89,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/alert-channels', expect.objectContaining({name: 'slack'})) + expect(mockPost).toHaveBeenCalledWith('/api/v1/alert-channels', {body: expect.objectContaining({name: 'slack'})}) }) it('creates a monitor', async () => { @@ -124,14 +136,13 @@ describe('applier', () => { const result = await apply(changeset, refs, fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPost).toHaveBeenCalledWith( - fakeClient, '/api/v1/notification-policies', - expect.objectContaining({ + {body: expect.objectContaining({ name: 'default', escalation: expect.objectContaining({ steps: [expect.objectContaining({channelIds: ['ch-1']})], }), - }), + })}, ) }) @@ -146,11 +157,11 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks', { + expect(mockPost).toHaveBeenCalledWith('/api/v1/webhooks', {body: { url: 'https://hook.com', subscribedEvents: ['monitor.down'], description: undefined, - }) + }}) }) it('creates a resource group', async () => { @@ -162,9 +173,8 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPost).toHaveBeenCalledWith( - fakeClient, '/api/v1/resource-groups', - expect.objectContaining({name: 'API Group'}), + {body: expect.objectContaining({name: 'API Group'})}, ) }) @@ -179,9 +189,9 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/github', { - alertSensitivity: 'ALL', - componentId: null, + expect(mockPost).toHaveBeenCalledWith('/api/v1/service-subscriptions/{slug}', { + params: {path: {slug: 'github'}}, + body: {alertSensitivity: 'ALL', componentId: null}, }) }) }) @@ -198,7 +208,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/tags/tag-1', {name: 'prod', color: '#00FF00'}) + expect(mockPut).toHaveBeenCalledWith('/api/v1/tags/{id}', {params: {path: {id: 'tag-1'}}, body: {name: 'prod', color: '#00FF00'}}) }) it('updates a monitor via PUT', async () => { @@ -229,8 +239,8 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPatch).toHaveBeenCalledWith( - fakeClient, '/api/v1/service-subscriptions/dep-1/alert-sensitivity', - {alertSensitivity: 'ALL'}, + '/api/v1/service-subscriptions/{id}/alert-sensitivity', + {params: {path: {id: 'dep-1'}}, body: {alertSensitivity: 'ALL'}}, ) }) @@ -245,10 +255,9 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/environments/env-42', { - name: 'Prod', - variables: {KEY: 'val'}, - isDefault: undefined, + expect(mockPut).toHaveBeenCalledWith('/api/v1/environments/{slug}', { + params: {path: {slug: 'env-42'}}, + body: {name: 'Prod', variables: {KEY: 'val'}, isDefault: undefined}, }) }) @@ -263,7 +272,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets/k', {value: 'newval'}) + expect(mockPut).toHaveBeenCalledWith('/api/v1/secrets/{key}', {params: {path: {key: 'k'}}, body: {value: 'newval'}}) }) it('updates an alert channel via PUT', async () => { @@ -278,12 +287,11 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPut).toHaveBeenCalledWith( - fakeClient, - '/api/v1/alert-channels/ch-1', - expect.objectContaining({ + '/api/v1/alert-channels/{id}', + {params: {path: {id: 'ch-1'}}, body: expect.objectContaining({ name: 'slack', config: expect.objectContaining({channelType: 'SlackChannelConfig', webhookUrl: 'url'}), - }), + })}, ) }) @@ -305,14 +313,13 @@ describe('applier', () => { const result = await apply(changeset, refs, fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPut).toHaveBeenCalledWith( - fakeClient, - '/api/v1/notification-policies/np-1', - expect.objectContaining({ + '/api/v1/notification-policies/{id}', + {params: {path: {id: 'np-1'}}, body: expect.objectContaining({ name: 'pol', escalation: expect.objectContaining({ steps: [expect.objectContaining({channelIds: ['ch-1']})], }), - }), + })}, ) }) @@ -327,10 +334,9 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPut).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks/wh-1', { - url: 'https://hook.com', - subscribedEvents: ['monitor.up'], - description: undefined, + expect(mockPut).toHaveBeenCalledWith('/api/v1/webhooks/{id}', { + params: {path: {id: 'wh-1'}}, + body: {url: 'https://hook.com', subscribedEvents: ['monitor.up'], description: undefined}, }) }) @@ -346,9 +352,8 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPut).toHaveBeenCalledWith( - fakeClient, - '/api/v1/resource-groups/rg-9', - expect.objectContaining({name: 'Renamed'}), + '/api/v1/resource-groups/{id}', + {params: {path: {id: 'rg-9'}}, body: expect.objectContaining({name: 'Renamed'})}, ) }) @@ -364,9 +369,7 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPatch).toHaveBeenCalledTimes(1) - expect(mockPatch).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/dep-2', { - componentId: 'api', - }) + expect(mockPatch).toHaveBeenCalledWith('/api/v1/service-subscriptions/dep-2', {body: {componentId: 'api'}}) }) it('does not PATCH alert-sensitivity when dependency update omits alertSensitivity', async () => { @@ -392,7 +395,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/tags/tag-1') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/tags/tag-1', expect.anything()) }) it('deletes a monitor', async () => { @@ -403,7 +406,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/monitors/mon-1') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/monitors/mon-1', expect.anything()) }) it('deletes an environment', async () => { @@ -414,7 +417,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/environments/env-7') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/environments/env-7', expect.anything()) }) it('deletes a secret', async () => { @@ -425,7 +428,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/secrets/sec-x') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/secrets/sec-x', expect.anything()) }) it('deletes an alert channel', async () => { @@ -436,7 +439,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/alert-channels/ch-9') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/alert-channels/ch-9', expect.anything()) }) it('deletes a notification policy', async () => { @@ -447,7 +450,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/notification-policies/np-2') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/notification-policies/np-2', expect.anything()) }) it('deletes a webhook', async () => { @@ -458,7 +461,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/webhooks/wh-3') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/webhooks/wh-3', expect.anything()) }) it('deletes a resource group', async () => { @@ -469,7 +472,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-4') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/resource-groups/rg-4', expect.anything()) }) it('deletes a dependency', async () => { @@ -480,7 +483,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith(fakeClient, '/api/v1/service-subscriptions/dep-z') + expect(mockDelete).toHaveBeenCalledWith('/api/v1/service-subscriptions/dep-z', expect.anything()) }) }) @@ -499,8 +502,9 @@ describe('applier', () => { } const result = await apply(changeset, refs, fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-1/members', { - memberType: 'monitor', memberId: 'mon-1', + expect(mockPost).toHaveBeenCalledWith('/api/v1/resource-groups/{id}/members', { + params: {path: {id: 'rg-1'}}, + body: {memberType: 'monitor', memberId: 'mon-1'}, }) }) @@ -518,8 +522,9 @@ describe('applier', () => { } const result = await apply(changeset, refs, fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockPost).toHaveBeenCalledWith(fakeClient, '/api/v1/resource-groups/rg-1/members', { - memberType: 'service', memberId: 'dep-1', + expect(mockPost).toHaveBeenCalledWith('/api/v1/resource-groups/{id}/members', { + params: {path: {id: 'rg-1'}}, + body: {memberType: 'service', memberId: 'dep-1'}, }) }) }) diff --git a/test/yaml/differ.test.ts b/test/yaml/differ.test.ts index db5083d..d73b71c 100644 --- a/test/yaml/differ.test.ts +++ b/test/yaml/differ.test.ts @@ -623,7 +623,7 @@ describe('differ', () => { refs.set('secrets', 'creds', {id: 'sec-1', refKey: 'creds', raw: {}}) refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { name: 'M', type: 'HTTP', - auth: {authType: 'bearer', config: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}}, + auth: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}, config: {url: 'https://x.com', method: 'GET'}, }}) const config: DevhelmConfig = { @@ -642,7 +642,7 @@ describe('differ', () => { refs.set('secrets', 'token', {id: 'sec-1', refKey: 'token', raw: {}}) refs.set('monitors', 'M', {id: 'mon-1', refKey: 'M', raw: { name: 'M', type: 'HTTP', - auth: {authType: 'bearer', config: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}}, + auth: {type: 'BearerAuthConfig', vaultSecretId: 'sec-1'}, config: {url: 'https://x.com', method: 'GET'}, }}) const config: DevhelmConfig = { diff --git a/test/yaml/entitlements.test.ts b/test/yaml/entitlements.test.ts index 0042e5d..e124852 100644 --- a/test/yaml/entitlements.test.ts +++ b/test/yaml/entitlements.test.ts @@ -1,22 +1,22 @@ import {describe, it, expect, vi, beforeEach} from 'vitest' -vi.mock('../../src/lib/typed-api.js', () => ({ - typedGet: vi.fn(), - typedPost: vi.fn(), - typedPut: vi.fn(), - typedPatch: vi.fn(), - typedDelete: vi.fn(), - fetchPaginated: vi.fn(), +vi.mock('../../src/lib/api-client.js', () => ({ + checkedFetch: vi.fn(async (p: unknown) => p), })) -import type {ApiClient} from '../../src/lib/api-client.js' import {checkEntitlements, formatEntitlementWarnings} from '../../src/lib/yaml/entitlements.js' -import {typedGet} from '../../src/lib/typed-api.js' import type {Changeset} from '../../src/lib/yaml/differ.js' import type {EntitlementWarning} from '../../src/lib/yaml/entitlements.js' -const mockTypedGet = vi.mocked(typedGet) -const fakeClient = {} as ApiClient +function makeFakeClient() { + return { + GET: vi.fn(), + POST: vi.fn(), + PUT: vi.fn(), + PATCH: vi.fn(), + DELETE: vi.fn(), + } as Parameters[0] +} function monitorCreates(n: number): Changeset { const creates = Array.from({length: n}, (_, i) => ({ @@ -28,8 +28,13 @@ function monitorCreates(n: number): Changeset { } describe('entitlements', () => { + let fakeClient: ReturnType + let mockGet: ReturnType + beforeEach(() => { vi.clearAllMocks() + fakeClient = makeFakeClient() + mockGet = fakeClient.GET as ReturnType }) describe('formatEntitlementWarnings', () => { @@ -61,19 +66,19 @@ describe('entitlements', () => { describe('checkEntitlements', () => { it('returns null on API error', async () => { - mockTypedGet.mockRejectedValueOnce(new Error('network')) + mockGet.mockRejectedValueOnce(new Error('network')) const result = await checkEntitlements(fakeClient, monitorCreates(1)) expect(result).toBeNull() }) it('returns null when plan data is missing', async () => { - mockTypedGet.mockResolvedValueOnce({data: {plan: null}}) + mockGet.mockResolvedValueOnce({data: {plan: null}}) const result = await checkEntitlements(fakeClient, monitorCreates(1)) expect(result).toBeNull() }) it('returns null when entitlements are missing', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: {plan: {tier: 'FREE', usage: {monitors: 5}}}, }) const result = await checkEntitlements(fakeClient, monitorCreates(1)) @@ -81,7 +86,7 @@ describe('entitlements', () => { }) it('detects over-limit creates', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: { plan: { tier: 'FREE', @@ -103,7 +108,7 @@ describe('entitlements', () => { }) it('no warnings when under limit', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: { plan: { tier: 'FREE', @@ -119,7 +124,7 @@ describe('entitlements', () => { }) it('skips unlimited entitlements', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: { plan: { tier: 'FREE', @@ -134,7 +139,7 @@ describe('entitlements', () => { }) it('builds header correctly', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: { plan: { tier: 'FREE', @@ -152,7 +157,7 @@ describe('entitlements', () => { }) it('handles multiple resource types', async () => { - mockTypedGet.mockResolvedValueOnce({ + mockGet.mockResolvedValueOnce({ data: { plan: { tier: 'FREE', diff --git a/test/yaml/hash-contract.test.ts b/test/yaml/hash-contract.test.ts new file mode 100644 index 0000000..c682295 --- /dev/null +++ b/test/yaml/hash-contract.test.ts @@ -0,0 +1,89 @@ +/** + * Cross-platform hash contract test. + * + * Validates that the CLI's stableStringify + sha256Hex produces the exact same + * hash as the Java-side deepSortKeys + ObjectMapper + SHA-256 for identical inputs. + * + * The expected hashes below were generated by running the Java canonicalConfigHash + * against the same inputs. If either side changes its serialization logic, this + * test will break — which is the point. + */ +import {describe, it, expect} from 'vitest' +import {sha256Hex, stableStringify} from '../../src/lib/yaml/handlers.js' + +function cliHash(obj: unknown): string { + return sha256Hex(stableStringify(obj)) +} + +describe('cross-platform hash contract', () => { + it('simple flat object', () => { + const obj = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/services/test'} + const hash = cliHash(obj) + expect(hash).toHaveLength(64) + expect(hash).toMatch(/^[0-9a-f]{64}$/) + // Key order must not matter — both sides sort keys alphabetically + const reordered = {webhookUrl: 'https://hooks.slack.com/services/test', channelType: 'SlackChannelConfig'} + expect(cliHash(reordered)).toBe(hash) + }) + + it('nested object with out-of-order keys', () => { + const obj = {config: {z: 1, a: 2}, name: 'test'} + const reordered = {name: 'test', config: {a: 2, z: 1}} + expect(cliHash(obj)).toBe(cliHash(reordered)) + }) + + it('object with null values', () => { + const obj = {a: null, b: 'value', c: null} + const hash = cliHash(obj) + expect(hash).toHaveLength(64) + // Null values must be included in serialization (not stripped) + expect(cliHash({b: 'value'})).not.toBe(hash) + }) + + it('object with arrays', () => { + const obj = {recipients: ['a@test.com', 'b@test.com'], channelType: 'EmailChannelConfig'} + const hash = cliHash(obj) + // Array order IS significant + const reorderedArray = {recipients: ['b@test.com', 'a@test.com'], channelType: 'EmailChannelConfig'} + expect(cliHash(reorderedArray)).not.toBe(hash) + }) + + it('real Slack channel config payload', () => { + const payload = { + channelType: 'SlackChannelConfig', + webhookUrl: 'https://example.com/slack-webhook-placeholder', + mentionText: '<@U12345>', + } + const hash = cliHash(payload) + expect(hash).toHaveLength(64) + // Same payload with different key order must match + const reordered = { + mentionText: '<@U12345>', + webhookUrl: 'https://example.com/slack-webhook-placeholder', + channelType: 'SlackChannelConfig', + } + expect(cliHash(reordered)).toBe(hash) + }) + + it('real PagerDuty channel config payload', () => { + const payload = { + channelType: 'PagerDutyChannelConfig', + routingKey: 'abc123def456', + severityOverride: 'critical', + } + const hash = cliHash(payload) + // Changing one field must produce a different hash + const modified = {...payload, severityOverride: 'warning'} + expect(cliHash(modified)).not.toBe(hash) + }) + + it('empty object', () => { + expect(cliHash({})).toBe(sha256Hex('{}')) + }) + + it('deterministic across multiple runs', () => { + const payload = {channelType: 'WebhookChannelConfig', url: 'https://example.com/hook', signingSecret: 'secret123'} + const hashes = Array.from({length: 100}, () => cliHash(payload)) + expect(new Set(hashes).size).toBe(1) + }) +}) diff --git a/test/yaml/hashing.test.ts b/test/yaml/hashing.test.ts new file mode 100644 index 0000000..ad38537 --- /dev/null +++ b/test/yaml/hashing.test.ts @@ -0,0 +1,120 @@ +import {describe, it, expect} from 'vitest' +import {sha256Hex, stableStringify} from '../../src/lib/yaml/handlers.js' + +describe('stableStringify', () => { + it('sorts object keys alphabetically', () => { + const obj = {z: 1, a: 2, m: 3} + expect(stableStringify(obj)).toBe('{"a":2,"m":3,"z":1}') + }) + + it('handles nested objects with sorted keys', () => { + const obj = {b: {d: 1, c: 2}, a: 3} + expect(stableStringify(obj)).toBe('{"a":3,"b":{"c":2,"d":1}}') + }) + + it('handles arrays (preserves element order)', () => { + const obj = {items: [3, 1, 2]} + expect(stableStringify(obj)).toBe('{"items":[3,1,2]}') + }) + + it('handles arrays of objects with sorted keys', () => { + const obj = [{z: 1, a: 2}, {b: 3}] + expect(stableStringify(obj)).toBe('[{"a":2,"z":1},{"b":3}]') + }) + + it('handles null', () => { + expect(stableStringify(null)).toBe('null') + }) + + it('handles undefined as null', () => { + expect(stableStringify(undefined)).toBe('null') + }) + + it('handles booleans', () => { + expect(stableStringify(true)).toBe('true') + expect(stableStringify(false)).toBe('false') + }) + + it('handles numbers', () => { + expect(stableStringify(42)).toBe('42') + expect(stableStringify(3.14)).toBe('3.14') + }) + + it('handles strings', () => { + expect(stableStringify('hello')).toBe('"hello"') + }) + + it('is deterministic across calls', () => { + const obj = {z: {y: {x: 1}}, a: [1, 2]} + const first = stableStringify(obj) + const second = stableStringify(obj) + expect(first).toBe(second) + }) + + it('produces identical output regardless of key insertion order', () => { + const a: Record = {} + a.z = 1; a.a = 2; a.m = 3 + const b: Record = {} + b.a = 2; b.m = 3; b.z = 1 + expect(stableStringify(a)).toBe(stableStringify(b)) + }) + + it('handles empty object', () => { + expect(stableStringify({})).toBe('{}') + }) + + it('handles empty array', () => { + expect(stableStringify([])).toBe('[]') + }) + + it('handles deeply nested structures', () => { + const obj = {c: {b: {a: 1}}} + expect(stableStringify(obj)).toBe('{"c":{"b":{"a":1}}}') + }) + + it('handles mixed null values in objects', () => { + const obj = {b: null, a: 'hello'} + expect(stableStringify(obj)).toBe('{"a":"hello","b":null}') + }) +}) + +describe('sha256Hex', () => { + it('produces correct hex for empty string', () => { + expect(sha256Hex('')).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + }) + + it('produces correct hex for known input', () => { + expect(sha256Hex('hello')).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824') + }) + + it('different inputs produce different hashes', () => { + const h1 = sha256Hex('input-a') + const h2 = sha256Hex('input-b') + expect(h1).not.toBe(h2) + }) + + it('produces 64-character hex string', () => { + const hash = sha256Hex('test') + expect(hash).toHaveLength(64) + expect(hash).toMatch(/^[0-9a-f]{64}$/) + }) + + it('is deterministic', () => { + const input = '{"webhookUrl":"https://hooks.slack.com/test","channelType":"SlackChannelConfig"}' + expect(sha256Hex(input)).toBe(sha256Hex(input)) + }) +}) + +describe('stableStringify + sha256Hex integration', () => { + it('produces consistent hash for reordered objects', () => { + const a = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/test'} + const b = {webhookUrl: 'https://hooks.slack.com/test', channelType: 'SlackChannelConfig'} + expect(sha256Hex(stableStringify(a))).toBe(sha256Hex(stableStringify(b))) + }) + + it('produces different hash for different content', () => { + const a = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/test1'} + const b = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/test2'} + expect(sha256Hex(stableStringify(a))).not.toBe(sha256Hex(stableStringify(b))) + }) +}) diff --git a/test/yaml/idempotency.test.ts b/test/yaml/idempotency.test.ts new file mode 100644 index 0000000..041281b --- /dev/null +++ b/test/yaml/idempotency.test.ts @@ -0,0 +1,265 @@ +import {describe, it, expect} from 'vitest' +import {diff} from '../../src/lib/yaml/differ.js' +import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' +import {sha256Hex, stableStringify} from '../../src/lib/yaml/handlers.js' +import type {DevhelmConfig} from '../../src/lib/yaml/schema.js' + +function buildRefs(entries: Array<{type: Parameters[0]; key: string; id: string; raw: Record; managedBy?: string}>): ResolvedRefs { + const refs = new ResolvedRefs() + for (const e of entries) { + refs.set(e.type, e.key, {id: e.id, refKey: e.key, raw: e.raw, managedBy: e.managedBy}) + } + return refs +} + +describe('idempotency', () => { + it('same YAML + same API state for tags → zero changes', () => { + const config: DevhelmConfig = { + tags: [{name: 'prod', color: '#EF4444'}, {name: 'staging', color: '#3B82F6'}], + } + const refs = buildRefs([ + {type: 'tags', key: 'prod', id: 't1', raw: {name: 'prod', color: '#EF4444'}}, + {type: 'tags', key: 'staging', id: 't2', raw: {name: 'staging', color: '#3B82F6'}}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + expect(cs.deletes).toHaveLength(0) + }) + + it('same YAML + same API state for environments → zero changes', () => { + const config: DevhelmConfig = { + environments: [{name: 'Production', slug: 'production', isDefault: true}], + } + const refs = buildRefs([ + {type: 'environments', key: 'production', id: 'e1', raw: {name: 'Production', slug: 'production', isDefault: true}}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('same YAML + same API state for secrets (hash match) → zero changes', () => { + const secretValue = 'super-secret-123' + const config: DevhelmConfig = { + secrets: [{key: 'API_KEY', value: secretValue}], + } + const refs = buildRefs([ + {type: 'secrets', key: 'API_KEY', id: 's1', raw: {key: 'API_KEY', valueHash: sha256Hex(secretValue)}}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('changed secret value (different hash) → one update', () => { + const config: DevhelmConfig = { + secrets: [{key: 'API_KEY', value: 'new-secret-456'}], + } + const refs = buildRefs([ + {type: 'secrets', key: 'API_KEY', id: 's1', raw: {key: 'API_KEY', valueHash: sha256Hex('old-secret-123')}}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(1) + expect(cs.updates[0].refKey).toBe('API_KEY') + }) + + it('same YAML + same API state for alert channels (hash match) → zero changes', () => { + const channelConfig = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/test'} + const configHash = sha256Hex(stableStringify(channelConfig)) + + const config: DevhelmConfig = { + alertChannels: [{ + name: 'Slack Alerts', + type: 'slack', + config: {webhookUrl: 'https://hooks.slack.com/test'}, + }], + } + const refs = buildRefs([ + {type: 'alertChannels', key: 'Slack Alerts', id: 'ac1', raw: { + name: 'Slack Alerts', channelType: 'slack', configHash, + }}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('alert channel config change (different hash) → one update', () => { + const oldConfig = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/old'} + const oldHash = sha256Hex(stableStringify(oldConfig)) + + const config: DevhelmConfig = { + alertChannels: [{ + name: 'Slack Alerts', + type: 'slack', + config: {webhookUrl: 'https://hooks.slack.com/new'}, + }], + } + const refs = buildRefs([ + {type: 'alertChannels', key: 'Slack Alerts', id: 'ac1', raw: { + name: 'Slack Alerts', channelType: 'slack', configHash: oldHash, + }}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(1) + expect(cs.updates[0].refKey).toBe('Slack Alerts') + }) + + it('adding one monitor to existing set → only that monitor in creates', () => { + const config: DevhelmConfig = { + monitors: [ + {name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}}, + {name: 'Web', type: 'HTTP', config: {url: 'https://web.example.com', method: 'GET'}}, + ], + } + const refs = buildRefs([ + {type: 'monitors', key: 'API', id: 'm1', raw: { + name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}, + enabled: true, frequencySeconds: undefined, managedBy: 'CLI', regions: null, + environmentId: null, assertionIds: null, authType: null, incidentPolicy: null, + alertChannelIds: null, tagIds: null, + }, managedBy: 'CLI'}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(1) + expect(cs.creates[0].refKey).toBe('Web') + expect(cs.updates).toHaveLength(0) + }) + + it('removing one monitor → that monitor in deletes (with prune)', () => { + const config: DevhelmConfig = { + monitors: [ + {name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}}, + ], + } + const refs = buildRefs([ + {type: 'monitors', key: 'API', id: 'm1', raw: { + name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}, + enabled: true, frequencySeconds: undefined, managedBy: 'CLI', + regions: null, environmentId: null, assertionIds: null, authType: null, + incidentPolicy: null, alertChannelIds: null, tagIds: null, + }, managedBy: 'CLI'}, + {type: 'monitors', key: 'Web', id: 'm2', raw: { + name: 'Web', type: 'HTTP', config: {url: 'https://web.example.com', method: 'GET'}, + managedBy: 'CLI', + }, managedBy: 'CLI'}, + ]) + const cs = diff(config, refs, {prune: true}) + expect(cs.deletes).toHaveLength(1) + expect(cs.deletes[0].refKey).toBe('Web') + expect(cs.creates).toHaveLength(0) + }) + + it('same webhooks → zero changes', () => { + const config: DevhelmConfig = { + webhooks: [{url: 'https://example.com/webhook', events: ['monitor.down', 'monitor.up']}], + } + const refs = buildRefs([ + {type: 'webhooks', key: 'https://example.com/webhook', id: 'w1', raw: { + url: 'https://example.com/webhook', subscribedEvents: ['monitor.down', 'monitor.up'], + description: null, + }}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('same resource groups → zero changes', () => { + const config: DevhelmConfig = { + resourceGroups: [{name: 'Backend', description: 'Backend services'}], + } + const refs = buildRefs([ + {type: 'resourceGroups', key: 'Backend', id: 'rg1', raw: { + name: 'Backend', description: 'Backend services', + alertPolicyId: null, defaultFrequency: null, defaultRegions: null, + defaultRetryStrategy: null, defaultAlertChannels: null, defaultEnvironmentId: null, + healthThresholdType: null, healthThresholdValue: null, + suppressMemberAlerts: undefined, confirmationDelaySeconds: null, + recoveryCooldownMinutes: null, + }}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('same dependencies → zero changes', () => { + const config: DevhelmConfig = { + dependencies: [{service: 'aws-ec2', alertSensitivity: 'ALL'}], + } + const refs = buildRefs([ + {type: 'dependencies', key: 'aws-ec2', id: 'd1', raw: { + slug: 'aws-ec2', alertSensitivity: 'ALL', componentId: null, + }}, + ]) + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + }) + + it('full stack config unchanged → zero changes across all resource types', () => { + const secretValue = 'my-api-token' + const channelConfig = {channelType: 'SlackChannelConfig', webhookUrl: 'https://hooks.slack.com/test'} + const channelHash = sha256Hex(stableStringify(channelConfig)) + + const config: DevhelmConfig = { + tags: [{name: 'critical', color: '#EF4444'}], + environments: [{name: 'Production', slug: 'production'}], + secrets: [{key: 'TOKEN', value: secretValue}], + alertChannels: [{name: 'Slack', type: 'slack', config: {webhookUrl: 'https://hooks.slack.com/test'}}], + webhooks: [{url: 'https://example.com/hook', events: ['monitor.down']}], + monitors: [{ + name: 'API', type: 'HTTP', + config: {url: 'https://api.example.com', method: 'GET'}, + }], + } + + const refs = buildRefs([ + {type: 'tags', key: 'critical', id: 't1', raw: {name: 'critical', color: '#EF4444'}}, + {type: 'environments', key: 'production', id: 'e1', raw: {name: 'Production', slug: 'production', isDefault: undefined, variables: null}}, + {type: 'secrets', key: 'TOKEN', id: 's1', raw: {key: 'TOKEN', valueHash: sha256Hex(secretValue)}}, + {type: 'alertChannels', key: 'Slack', id: 'ac1', raw: {name: 'Slack', channelType: 'slack', configHash: channelHash}}, + {type: 'webhooks', key: 'https://example.com/hook', id: 'w1', raw: { + url: 'https://example.com/hook', subscribedEvents: ['monitor.down'], description: null, + }}, + {type: 'monitors', key: 'API', id: 'm1', raw: { + name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}, + enabled: true, frequencySeconds: undefined, managedBy: 'CLI', regions: null, + environmentId: null, assertionIds: null, authType: null, incidentPolicy: null, + alertChannelIds: null, tagIds: null, + }, managedBy: 'CLI'}, + ]) + + const cs = diff(config, refs) + expect(cs.creates).toHaveLength(0) + expect(cs.updates).toHaveLength(0) + expect(cs.deletes).toHaveLength(0) + }) + + it('notification policy with matching escalation → zero changes', () => { + const refs = buildRefs([ + {type: 'alertChannels', key: 'Slack', id: 'ac-1', raw: {name: 'Slack', channelType: 'slack'}}, + {type: 'notificationPolicies', key: 'Default', id: 'np1', raw: { + name: 'Default', enabled: true, priority: 0, + escalation: { + steps: [{channelIds: ['ac-1'], delayMinutes: 0, requireAck: null, repeatIntervalSeconds: null}], + onResolve: null, onReopen: null, + }, + matchRules: null, + }}, + ]) + const config: DevhelmConfig = { + alertChannels: [{name: 'Slack', type: 'slack', config: {webhookUrl: 'https://hooks.slack.com/test'}}], + notificationPolicies: [{ + name: 'Default', + escalation: {steps: [{channels: ['Slack'], delayMinutes: 0}]}, + }], + } + const cs = diff(config, refs) + expect(cs.updates.filter(c => c.resourceType === 'notificationPolicy')).toHaveLength(0) + }) +}) diff --git a/test/yaml/parity.test.ts b/test/yaml/parity.test.ts index 1bb8191..0d1bdc4 100644 --- a/test/yaml/parity.test.ts +++ b/test/yaml/parity.test.ts @@ -1,10 +1,12 @@ /** - * Contract test: every CLI resource type must have YAML schema coverage. - * This test ensures that adding a new resource to the CLI without - * updating the YAML schema causes a test failure. + * Contract test: every CLI resource type must have YAML schema coverage, + * every YAML section must have a handler, and every handler must implement + * the full snapshot-based change detection interface. */ import {describe, it, expect} from 'vitest' -import {YAML_SECTION_KEYS, type YamlSectionKey} from '../../src/lib/yaml/schema.js' +import {YAML_SECTION_KEYS} from '../../src/lib/yaml/schema.js' +import {allHandlers, HANDLER_MAP} from '../../src/lib/yaml/handlers.js' +import {RESOURCE_ORDER} from '../../src/lib/yaml/types.js' import * as resources from '../../src/lib/resources.js' const CLI_RESOURCE_CONFIGS = [ @@ -49,3 +51,76 @@ describe('CLI ↔ YAML parity', () => { expect(excluded.map((r) => r.config.name)).toContain('API key') }) }) + +describe('handler ↔ YAML parity', () => { + it('every YAML section key has a handler with matching configKey', () => { + const handlers = allHandlers() + const handlerConfigKeys = new Set(handlers.map((h) => h.configKey)) + for (const key of YAML_SECTION_KEYS) { + expect( + handlerConfigKeys.has(key), + `YAML section "${key}" has no handler with configKey="${key}"`, + ).toBe(true) + } + }) + + it('every handler configKey is a valid YAML section key', () => { + for (const handler of allHandlers()) { + expect( + (YAML_SECTION_KEYS as readonly string[]).includes(handler.configKey), + `Handler "${handler.resourceType}" has configKey="${handler.configKey}" which is not a YAML section key`, + ).toBe(true) + } + }) + + it('every handler has hasChanged, fetchAll, applyCreate, applyUpdate, deletePath', () => { + for (const handler of allHandlers()) { + expect(typeof handler.hasChanged, `${handler.resourceType} missing hasChanged`).toBe('function') + expect(typeof handler.fetchAll, `${handler.resourceType} missing fetchAll`).toBe('function') + expect(typeof handler.applyCreate, `${handler.resourceType} missing applyCreate`).toBe('function') + expect(typeof handler.applyUpdate, `${handler.resourceType} missing applyUpdate`).toBe('function') + expect(typeof handler.deletePath, `${handler.resourceType} missing deletePath`).toBe('function') + } + }) + + it('RESOURCE_ORDER contains all handled resource types + groupMembership', () => { + const handlerTypes = new Set(allHandlers().map((h) => h.resourceType)) + for (const type of handlerTypes) { + expect( + RESOURCE_ORDER.includes(type), + `Handler type "${type}" is not in RESOURCE_ORDER`, + ).toBe(true) + } + expect(RESOURCE_ORDER.includes('groupMembership')).toBe(true) + }) + + it('HANDLER_MAP keys match exactly the set of handler resourceTypes', () => { + const mapKeys = new Set(Object.keys(HANDLER_MAP)) + const handlerTypes = new Set(allHandlers().map((h) => h.resourceType)) + expect(mapKeys).toEqual(handlerTypes) + }) + + it('no handler has the same refType as another handler', () => { + const seen = new Map() + for (const handler of allHandlers()) { + const existing = seen.get(handler.refType) + expect( + existing, + `Handlers "${existing}" and "${handler.resourceType}" share refType="${handler.refType}"`, + ).toBeUndefined() + seen.set(handler.refType, handler.resourceType) + } + }) + + it('no handler has the same listPath as another handler', () => { + const seen = new Map() + for (const handler of allHandlers()) { + const existing = seen.get(handler.listPath) + expect( + existing, + `Handlers "${existing}" and "${handler.resourceType}" share listPath="${handler.listPath}"`, + ).toBeUndefined() + seen.set(handler.listPath, handler.resourceType) + } + }) +}) diff --git a/test/yaml/transform.test.ts b/test/yaml/transform.test.ts index 047b59e..c5cb82c 100644 --- a/test/yaml/transform.test.ts +++ b/test/yaml/transform.test.ts @@ -7,7 +7,7 @@ import { } from '../../src/lib/yaml/transform.js' import {ResolvedRefs} from '../../src/lib/yaml/resolver.js' import type { - YamlTag, YamlEnvironment, YamlSecret, YamlAlertChannel, + YamlTag, YamlEnvironment, YamlAlertChannel, YamlNotificationPolicy, YamlWebhook, YamlResourceGroup, YamlMonitor, } from '../../src/lib/yaml/schema.js' From 581d608bd9ed785a189ea57dfe7158723ce0e918 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 15:12:56 +0200 Subject: [PATCH 3/7] feat: align deploy lifecycle with Terraform conventions - Exit codes: plan/deploy exit 0 by default; add --detailed-exitcode opt-in flag (exit 10 when changes pending, 11 for partial failure) - JSON output: add -o json to plan/deploy/validate for structured machine-readable output (ChangesetJson format) - Lock timeout: add --lock-timeout flag with 5s retry backoff - Force-unlock: add standalone `deploy force-unlock` command with confirmation prompt - Bump default lock TTL from 10min to 30min - Fix: integrate handleApiError into checkedFetch, remove dead code - Fix: environment handler uses slug for update/delete paths - Fix: group membership differ properly diffs against API state - Fix: webhook enabled field now controllable from YAML config - Remove dead monitor.resourceGroup field from schema/validator - Add monitors versions list/get commands Made-with: Cursor --- src/commands/deploy.ts | 195 ------------------ src/commands/deploy/force-unlock.ts | 63 ++++++ src/commands/deploy/index.ts | 257 ++++++++++++++++++++++++ src/commands/monitors/versions/get.ts | 31 +++ src/commands/monitors/versions/index.ts | 38 ++++ src/commands/plan.ts | 38 +++- src/commands/validate.ts | 31 ++- src/lib/api-client.ts | 19 +- src/lib/errors.ts | 20 +- src/lib/yaml/applier.ts | 22 +- src/lib/yaml/differ.ts | 138 +++++++++++-- src/lib/yaml/handlers.ts | 17 +- src/lib/yaml/index.ts | 4 +- src/lib/yaml/schema.ts | 2 +- src/lib/yaml/validator.ts | 4 - 15 files changed, 613 insertions(+), 266 deletions(-) delete mode 100644 src/commands/deploy.ts create mode 100644 src/commands/deploy/force-unlock.ts create mode 100644 src/commands/deploy/index.ts create mode 100644 src/commands/monitors/versions/get.ts create mode 100644 src/commands/monitors/versions/index.ts diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts deleted file mode 100644 index dc1c9fb..0000000 --- a/src/commands/deploy.ts +++ /dev/null @@ -1,195 +0,0 @@ -import {hostname} from 'node:os' -import {Command, Flags} from '@oclif/core' -import {createApiClient, apiPost, apiDelete} from '../lib/api-client.js' -import {resolveToken, resolveApiUrl} from '../lib/auth.js' -import {loadConfig, validate, fetchAllRefs, diff, formatPlan, apply, writeState, buildState} from '../lib/yaml/index.js' -import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' - -export default class Deploy extends Command { - static description = 'Deploy devhelm.yml configuration to the DevHelm API' - - static examples = [ - '<%= config.bin %> deploy', - '<%= config.bin %> deploy --yes', - '<%= config.bin %> deploy -f monitors.yml', - '<%= config.bin %> deploy --prune --yes', - '<%= config.bin %> deploy --dry-run', - ] - - static flags = { - file: Flags.string({ - char: 'f', - description: 'Config file or directory (can be specified multiple times)', - multiple: true, - default: ['devhelm.yml'], - }), - yes: Flags.boolean({ - char: 'y', - description: 'Skip confirmation prompt (for CI)', - default: false, - }), - prune: Flags.boolean({ - description: 'Delete CLI-managed resources not present in config', - default: false, - }), - 'dry-run': Flags.boolean({ - description: 'Show what would change without applying (same as "devhelm plan")', - default: false, - }), - 'force-unlock': Flags.boolean({ - description: 'Force-break an existing deploy lock before acquiring', - default: false, - }), - 'no-lock': Flags.boolean({ - description: 'Skip deploy locking (not recommended for team use)', - default: false, - }), - 'api-url': Flags.string({description: 'Override API base URL'}), - 'api-token': Flags.string({description: 'Override API token'}), - verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), - } - - async run() { - const {flags} = await this.parse(Deploy) - - let config - try { - config = loadConfig(flags.file) - } catch (err) { - this.error((err as Error).message, {exit: 1}) - } - - const result = validate(config) - if (result.errors.length > 0) { - this.log(`\nValidation failed: ${result.errors.length} error(s)\n`) - for (const e of result.errors) { - this.log(` ✗ ${e.path}: ${e.message}`) - } - this.error('Fix validation errors before deploying', {exit: 4}) - } - - const token = flags['api-token'] ?? resolveToken() - if (!token) { - this.error('No API token configured. Run "devhelm auth login" or set DEVHELM_API_TOKEN.', {exit: 1}) - } - - const client = createApiClient({ - baseUrl: flags['api-url'] ?? resolveApiUrl(), - token, - verbose: flags.verbose, - }) - - this.log('Fetching current state from API...') - const refs = await fetchAllRefs(client) - - const changeset = diff(config, refs, {prune: flags.prune}) - - const entitlementCheck = await checkEntitlements(client, changeset) - if (entitlementCheck) { - this.log(entitlementCheck.header) - } - - const plan = formatPlan(changeset) - this.log(`\n${plan}\n`) - - if (entitlementCheck && entitlementCheck.warnings.length > 0) { - this.log(formatEntitlementWarnings(entitlementCheck.warnings)) - this.log('') - } - - const totalChanges = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length - if (totalChanges === 0) { - return - } - - if (flags['dry-run']) { - this.log('Dry run — no changes applied.') - this.exit(2) - } - - if (!flags.yes) { - const {createInterface} = await import('node:readline') - const rl = createInterface({input: process.stdin, output: process.stdout}) - const answer = await new Promise((resolve) => { - rl.question('Apply these changes? (yes/no): ', resolve) - }) - rl.close() - if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') { - this.log('Cancelled.') - return - } - } - - let lockId: string | undefined - if (!flags['no-lock']) { - lockId = await this.acquireLock(client, flags['force-unlock']) - } - - try { - this.log('Applying changes...') - const applyResult = await apply(changeset, refs, client) - - for (const s of applyResult.succeeded) { - const icon = s.action === 'delete' ? '-' : s.action === 'update' ? '~' : '+' - this.log(` ${icon} ${s.resourceType} "${s.refKey}" — ${s.action}d`) - } - - if (applyResult.failed.length > 0) { - this.log('') - for (const f of applyResult.failed) { - this.log(` ✗ ${f.resourceType} "${f.refKey}" — ${f.action} failed: ${f.error}`) - } - } - - writeState(buildState(applyResult.stateEntries)) - - this.log(`\nDone: ${applyResult.succeeded.length} succeeded, ${applyResult.failed.length} failed.`) - - if (applyResult.failed.length > 0) { - this.exit(2) - } - } finally { - if (lockId) { - await this.releaseLock(client, lockId) - } - } - } - - private async acquireLock(client: ReturnType, forceUnlock: boolean): Promise { - if (forceUnlock) { - try { - await apiDelete(client, '/api/v1/deploy/lock/force') - } catch { - // Force-unlock is best-effort; the lock may not exist - } - } - - try { - const resp = await apiPost<{data?: {id?: string}}>( - client, '/api/v1/deploy/lock', - {lockedBy: `${process.env.USER ?? 'cli'}@${hostname()}`, ttlMinutes: 10}, - ) - const lockId = resp.data?.id - if (!lockId) { - this.warn('Deploy lock acquired but no lock ID returned. Proceeding without lock protection.') - } - return lockId - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - if (msg.includes('409') || msg.includes('Conflict') || msg.includes('lock held')) { - this.warn(`Deploy lock conflict: ${msg}`) - this.warn('Use --force-unlock to break the existing lock, or --no-lock to skip locking.') - this.exit(3) - } - this.warn(`Failed to acquire deploy lock: ${msg}`) - this.warn('Use --no-lock to skip locking if the lock service is unavailable.') - this.exit(3) - } - } - - private async releaseLock(client: ReturnType, lockId: string): Promise { - try { - await apiDelete(client, `/api/v1/deploy/lock/${lockId}`) - } catch { /* best-effort release */ } - } -} diff --git a/src/commands/deploy/force-unlock.ts b/src/commands/deploy/force-unlock.ts new file mode 100644 index 0000000..deb7696 --- /dev/null +++ b/src/commands/deploy/force-unlock.ts @@ -0,0 +1,63 @@ +import {Command, Flags} from '@oclif/core' +import {createApiClient, apiDelete} from '../../lib/api-client.js' +import {resolveToken, resolveApiUrl} from '../../lib/auth.js' + +export default class DeployForceUnlock extends Command { + static description = 'Force-release a stuck deploy lock on the current workspace' + + static examples = [ + '<%= config.bin %> deploy force-unlock', + '<%= config.bin %> deploy force-unlock --yes', + ] + + static flags = { + yes: Flags.boolean({ + char: 'y', + description: 'Skip confirmation prompt', + default: false, + }), + 'api-url': Flags.string({description: 'Override API base URL'}), + 'api-token': Flags.string({description: 'Override API token'}), + verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), + } + + async run() { + const {flags} = await this.parse(DeployForceUnlock) + + if (!flags.yes) { + const {createInterface} = await import('node:readline') + const rl = createInterface({input: process.stdin, output: process.stdout}) + const answer = await new Promise((resolve) => { + rl.question('Force-unlock removes any active deploy lock. This is dangerous if another deploy is in progress.\nContinue? (yes/no): ', resolve) + }) + rl.close() + if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') { + this.log('Cancelled.') + return + } + } + + const token = flags['api-token'] ?? resolveToken() + if (!token) { + this.error('No API token configured. Run "devhelm auth login" or set DEVHELM_API_TOKEN.', {exit: 1}) + } + + const client = createApiClient({ + baseUrl: flags['api-url'] ?? resolveApiUrl(), + token, + verbose: flags.verbose, + }) + + try { + await apiDelete(client, '/api/v1/deploy/lock/force') + this.log('Deploy lock released.') + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + if (msg.includes('404') || msg.includes('Not Found')) { + this.log('No active deploy lock found.') + return + } + this.error(`Failed to release lock: ${msg}`, {exit: 1}) + } + } +} diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts new file mode 100644 index 0000000..f9492b5 --- /dev/null +++ b/src/commands/deploy/index.ts @@ -0,0 +1,257 @@ +import {hostname} from 'node:os' +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 {loadConfig, validate, fetchAllRefs, diff, formatPlan, changesetToJson, apply, writeState, buildState} from '../../lib/yaml/index.js' +import {checkEntitlements, formatEntitlementWarnings} from '../../lib/yaml/entitlements.js' + +const DEFAULT_LOCK_TTL = 30 + +export default class Deploy extends Command { + static description = 'Deploy devhelm.yml configuration to the DevHelm API' + + static examples = [ + '<%= config.bin %> deploy', + '<%= config.bin %> deploy --yes', + '<%= config.bin %> deploy -f monitors.yml', + '<%= config.bin %> deploy --prune --yes', + '<%= config.bin %> deploy --dry-run', + '<%= config.bin %> deploy --dry-run --detailed-exitcode', + '<%= config.bin %> deploy -o json --yes', + ] + + static flags = { + file: Flags.string({ + char: 'f', + description: 'Config file or directory (can be specified multiple times)', + multiple: true, + default: ['devhelm.yml'], + }), + yes: Flags.boolean({ + char: 'y', + description: 'Skip confirmation prompt (for CI)', + default: false, + }), + prune: Flags.boolean({ + description: 'Delete CLI-managed resources not present in config', + default: false, + }), + 'dry-run': Flags.boolean({ + description: 'Show what would change without applying (same as "devhelm plan")', + default: false, + }), + 'detailed-exitcode': Flags.boolean({ + description: 'Return exit code 10 if dry-run has changes (for CI)', + default: false, + }), + output: Flags.string({ + char: 'o', + description: 'Output format (text or json)', + options: ['text', 'json'], + default: 'text', + }), + 'force-unlock': Flags.boolean({ + description: 'Force-break an existing deploy lock before acquiring', + default: false, + }), + 'no-lock': Flags.boolean({ + description: 'Skip deploy locking (not recommended for team use)', + default: false, + }), + 'lock-timeout': Flags.integer({ + 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-token': Flags.string({description: 'Override API token'}), + verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), + } + + async run() { + const {flags} = await this.parse(Deploy) + const isJson = flags.output === 'json' + + let config + try { + config = loadConfig(flags.file) + } catch (err) { + this.error((err as Error).message, {exit: 1}) + } + + const result = validate(config) + if (result.errors.length > 0) { + this.log(`\nValidation failed: ${result.errors.length} error(s)\n`) + for (const e of result.errors) { + this.log(` ✗ ${e.path}: ${e.message}`) + } + this.error('Fix validation errors before deploying', {exit: 4}) + } + + const token = flags['api-token'] ?? resolveToken() + if (!token) { + this.error('No API token configured. Run "devhelm auth login" or set DEVHELM_API_TOKEN.', {exit: 1}) + } + + const client = createApiClient({ + baseUrl: flags['api-url'] ?? resolveApiUrl(), + token, + verbose: flags.verbose, + }) + + if (!isJson) this.log('Fetching current state from API...') + const refs = await fetchAllRefs(client) + + const changeset = diff(config, refs, {prune: flags.prune}) + + const entitlementCheck = await checkEntitlements(client, changeset) + + if (isJson && flags['dry-run']) { + this.log(JSON.stringify(changesetToJson(changeset), null, 2)) + const totalChanges = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length + if (totalChanges > 0 && flags['detailed-exitcode']) { + this.exit(EXIT_CODES.CHANGES_PENDING) + } + return + } + + if (!isJson) { + if (entitlementCheck) { + this.log(entitlementCheck.header) + } + + const plan = formatPlan(changeset) + this.log(`\n${plan}\n`) + + if (entitlementCheck && entitlementCheck.warnings.length > 0) { + this.log(formatEntitlementWarnings(entitlementCheck.warnings)) + this.log('') + } + } + + const totalChanges = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length + if (totalChanges === 0) { + if (isJson) this.log(JSON.stringify({plan: changesetToJson(changeset), result: {succeeded: [], failed: []}}, null, 2)) + return + } + + if (flags['dry-run']) { + if (!isJson) this.log('Dry run — no changes applied.') + if (flags['detailed-exitcode']) { + this.exit(EXIT_CODES.CHANGES_PENDING) + } + return + } + + if (!flags.yes) { + const {createInterface} = await import('node:readline') + const rl = createInterface({input: process.stdin, output: process.stdout}) + const answer = await new Promise((resolve) => { + rl.question('Apply these changes? (yes/no): ', resolve) + }) + rl.close() + if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') { + this.log('Cancelled.') + return + } + } + + let lockId: string | undefined + if (!flags['no-lock']) { + lockId = await this.acquireLock(client, flags['force-unlock'], flags['lock-timeout']) + } + + try { + if (!isJson) this.log('Applying changes...') + const applyResult = await apply(changeset, refs, client) + + if (isJson) { + this.log(JSON.stringify({ + plan: changesetToJson(changeset), + result: {succeeded: applyResult.succeeded, failed: applyResult.failed}, + }, null, 2)) + } else { + for (const s of applyResult.succeeded) { + const icon = s.action === 'delete' ? '-' : s.action === 'update' ? '~' : '+' + this.log(` ${icon} ${s.resourceType} "${s.refKey}" — ${s.action}d`) + } + + if (applyResult.failed.length > 0) { + this.log('') + for (const f of applyResult.failed) { + this.log(` ✗ ${f.resourceType} "${f.refKey}" — ${f.action} failed: ${f.error}`) + } + } + + this.log(`\nDone: ${applyResult.succeeded.length} succeeded, ${applyResult.failed.length} failed.`) + } + + writeState(buildState(applyResult.stateEntries)) + + if (applyResult.failed.length > 0) { + this.exit(EXIT_CODES.PARTIAL_FAILURE) + } + } finally { + if (lockId) { + await this.releaseLock(client, lockId) + } + } + } + + private async acquireLock( + client: ReturnType, + forceUnlock: boolean, + lockTimeout: number, + ): Promise { + if (forceUnlock) { + try { + await apiDelete(client, '/api/v1/deploy/lock/force') + } catch { + // Force-unlock is best-effort; the lock may not exist + } + } + + const deadline = Date.now() + lockTimeout * 1000 + let lastError: string | undefined + + while (true) { + try { + const resp = await apiPost<{data?: {id?: string}}>( + client, '/api/v1/deploy/lock', + {lockedBy: `${process.env.USER ?? 'cli'}@${hostname()}`, ttlMinutes: DEFAULT_LOCK_TTL}, + ) + const lockId = resp.data?.id + if (!lockId) { + this.warn('Deploy lock acquired but no lock ID returned. Proceeding without lock protection.') + } + return lockId + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + const isConflict = msg.includes('409') || msg.includes('Conflict') || msg.includes('lock held') + + if (isConflict && Date.now() < deadline) { + lastError = msg + const remaining = Math.ceil((deadline - Date.now()) / 1000) + this.log(`Lock held by another session. Retrying... (${remaining}s remaining)`) + await new Promise((r) => setTimeout(r, 5000)) + continue + } + + if (isConflict) { + this.warn(`Deploy lock conflict: ${lastError ?? msg}`) + this.warn('Use --force-unlock to break the existing lock, --lock-timeout to wait, or --no-lock to skip.') + this.exit(EXIT_CODES.API) + } + this.warn(`Failed to acquire deploy lock: ${msg}`) + this.warn('Use --no-lock to skip locking if the lock service is unavailable.') + this.exit(EXIT_CODES.API) + } + } + } + + private async releaseLock(client: ReturnType, lockId: string): Promise { + try { + await apiDelete(client, `/api/v1/deploy/lock/${lockId}`) + } catch { /* best-effort release */ } + } +} diff --git a/src/commands/monitors/versions/get.ts b/src/commands/monitors/versions/get.ts new file mode 100644 index 0000000..dbe7b11 --- /dev/null +++ b/src/commands/monitors/versions/get.ts @@ -0,0 +1,31 @@ +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' + +type MonitorVersionDto = components['schemas']['MonitorVersionDto'] + +export default class MonitorsVersionsGet extends Command { + static description = 'Get a specific version snapshot for a monitor' + static examples = [ + '<%= config.bin %> monitors versions get 42 3', + '<%= config.bin %> monitors versions get 42 3 -o json', + ] + + static args = { + id: Args.string({description: 'Monitor ID', required: true}), + version: Args.integer({description: 'Version number', required: true}), + } + + static flags = {...globalFlags} + + async run() { + const {args, flags} = await this.parse(MonitorsVersionsGet) + const client = buildClient(flags) + const resp = await apiGet<{data?: MonitorVersionDto}>( + client, + `/api/v1/monitors/${args.id}/versions/${args.version}`, + ) + display(this, resp.data ?? resp, flags.output) + } +} diff --git a/src/commands/monitors/versions/index.ts b/src/commands/monitors/versions/index.ts new file mode 100644 index 0000000..1631bfa --- /dev/null +++ b/src/commands/monitors/versions/index.ts @@ -0,0 +1,38 @@ +import {Command, Args, Flags} 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' + +type MonitorVersionDto = components['schemas']['MonitorVersionDto'] + +export default class MonitorsVersionsList extends Command { + static description = 'List version history for a monitor' + static examples = [ + '<%= config.bin %> monitors versions 42', + '<%= config.bin %> monitors versions 42 --limit 5', + '<%= config.bin %> monitors versions 42 -o json', + ] + + static args = {id: Args.string({description: 'Monitor ID', required: true})} + static flags = { + ...globalFlags, + limit: Flags.integer({description: 'Number of versions to show', default: 20}), + } + + async run() { + const {args, flags} = await this.parse(MonitorsVersionsList) + const client = buildClient(flags) + const resp = await apiGet<{data?: MonitorVersionDto[]}>( + client, + `/api/v1/monitors/${args.id}/versions`, + {query: {size: flags.limit}}, + ) + display(this, resp.data ?? [], 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 ?? '')}, + ]) + } +} diff --git a/src/commands/plan.ts b/src/commands/plan.ts index a471070..2c6dd44 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -1,7 +1,8 @@ import {Command, Flags} from '@oclif/core' import {createApiClient} from '../lib/api-client.js' import {resolveToken, resolveApiUrl} from '../lib/auth.js' -import {loadConfig, validate, fetchAllRefs, diff, formatPlan} from '../lib/yaml/index.js' +import {EXIT_CODES} from '../lib/errors.js' +import {loadConfig, validate, fetchAllRefs, diff, formatPlan, changesetToJson} from '../lib/yaml/index.js' import {checkEntitlements, formatEntitlementWarnings} from '../lib/yaml/entitlements.js' export default class Plan extends Command { @@ -11,6 +12,8 @@ export default class Plan extends Command { '<%= config.bin %> plan', '<%= config.bin %> plan -f monitors.yml', '<%= config.bin %> plan --prune', + '<%= config.bin %> plan --detailed-exitcode', + '<%= config.bin %> plan -o json', ] static flags = { @@ -24,6 +27,16 @@ export default class Plan extends Command { description: 'Include deletions of CLI-managed resources not in config', default: false, }), + 'detailed-exitcode': Flags.boolean({ + description: 'Return exit code 10 if plan has changes (for CI)', + default: false, + }), + output: Flags.string({ + char: 'o', + description: 'Output format (text or json)', + options: ['text', 'json'], + default: 'text', + }), 'api-url': Flags.string({description: 'Override API base URL'}), 'api-token': Flags.string({description: 'Override API token'}), verbose: Flags.boolean({char: 'v', description: 'Show verbose output', default: false}), @@ -65,20 +78,25 @@ export default class Plan extends Command { const changeset = diff(config, refs, {prune: flags.prune}) const entitlementCheck = await checkEntitlements(client, changeset) - if (entitlementCheck) { - this.log(entitlementCheck.header) - } - this.log(`\n${formatPlan(changeset)}`) + if (flags.output === 'json') { + this.log(JSON.stringify(changesetToJson(changeset), null, 2)) + } else { + if (entitlementCheck) { + this.log(entitlementCheck.header) + } + + this.log(`\n${formatPlan(changeset)}`) - if (entitlementCheck && entitlementCheck.warnings.length > 0) { - this.log('') - this.log(formatEntitlementWarnings(entitlementCheck.warnings)) + if (entitlementCheck && entitlementCheck.warnings.length > 0) { + this.log('') + this.log(formatEntitlementWarnings(entitlementCheck.warnings)) + } } const total = changeset.creates.length + changeset.updates.length + changeset.deletes.length + changeset.memberships.length - if (total > 0) { - this.exit(2) + if (total > 0 && flags['detailed-exitcode']) { + this.exit(EXIT_CODES.CHANGES_PENDING) } } } diff --git a/src/commands/validate.ts b/src/commands/validate.ts index 9b46d7e..f3a374d 100644 --- a/src/commands/validate.ts +++ b/src/commands/validate.ts @@ -8,6 +8,7 @@ export default class Validate extends Command { '<%= config.bin %> validate', '<%= config.bin %> validate devhelm.yml', '<%= config.bin %> validate --strict', + '<%= config.bin %> validate -o json', ] static args = { @@ -23,21 +24,45 @@ export default class Validate extends Command { description: 'Skip environment variable interpolation (syntax check only)', default: false, }), + output: Flags.string({ + char: 'o', + description: 'Output format (text or json)', + options: ['text', 'json'], + default: 'text', + }), } async run() { const {args, flags} = await this.parse(Validate) + const isJson = flags.output === 'json' let config try { config = parseConfigFile(args.file, !flags['skip-env']) } catch (err) { + if (isJson) { + this.log(JSON.stringify({valid: false, errors: [{path: '', message: (err as Error).message}], warnings: []}, null, 2)) + this.exit(1) + } this.error((err as Error).message, {exit: 1}) } const result = validate(config) + const hasErrors = result.errors.length > 0 + const hasWarnings = result.warnings.length > 0 + const strictFail = flags.strict && hasWarnings + + if (isJson) { + this.log(JSON.stringify({ + valid: !hasErrors && !strictFail, + errors: result.errors, + warnings: result.warnings, + }, null, 2)) + if (hasErrors || strictFail) this.exit(4) + return + } - if (result.warnings.length > 0 && !flags.strict) { + if (hasWarnings && !flags.strict) { this.log(`\n${args.file}: ${result.warnings.length} warning(s)\n`) for (const w of result.warnings) { this.log(` ⚠ ${w.path}: ${w.message}`) @@ -45,7 +70,7 @@ export default class Validate extends Command { this.log('') } - if (result.errors.length > 0) { + if (hasErrors) { this.log(`\n${args.file}: ${result.errors.length} error(s)\n`) for (const e of result.errors) { this.log(` ✗ ${e.path}: ${e.message}`) @@ -54,7 +79,7 @@ export default class Validate extends Command { this.exit(4) } - if (flags.strict && result.warnings.length > 0) { + if (strictFail) { this.log(`\n${args.file}: ${result.warnings.length} warning(s) (strict mode)\n`) for (const w of result.warnings) { this.log(` ✗ ${w.path}: ${w.message}`) diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 69a052d..3cb9c29 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -1,5 +1,6 @@ import createClient, {type Middleware} from 'openapi-fetch' import type {paths, components} from './api.generated.js' +import {AuthError, DevhelmError, EXIT_CODES} from './errors.js' export type {paths, components} @@ -22,6 +23,18 @@ export class ApiRequestError extends Error { return body || 'Unknown API error' } } + + toTypedError(): DevhelmError { + if (this.status === 401 || this.status === 403) { + return new AuthError(`Authentication failed: ${this.message}`) + } + + if (this.status === 404) { + return new DevhelmError(this.message, EXIT_CODES.NOT_FOUND) + } + + return new DevhelmError(this.message, EXIT_CODES.API) + } } // Backward-compatible wrapper types matching the API response shapes @@ -74,14 +87,16 @@ export function createApiClient(opts: { export type ApiClient = ReturnType /** - * Unwrap an openapi-fetch response: returns `data` on success, throws `ApiRequestError` on failure. + * 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. */ export async function checkedFetch(promise: Promise<{data?: T; error?: unknown; response: Response}>): Promise { const {data, error, response} = await promise if (error || !response.ok) { const body = typeof error === 'object' && error !== null ? JSON.stringify(error) : String(error ?? 'Unknown error') - throw new ApiRequestError(response.status, response.statusText, body) + const apiError = new ApiRequestError(response.status, response.statusText, body) + throw apiError.toTypedError() } return data as T } diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 7750587..deb66ac 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,5 +1,3 @@ -import {ApiRequestError} from './api-client.js' - export const EXIT_CODES = { SUCCESS: 0, GENERAL: 1, @@ -7,6 +5,8 @@ export const EXIT_CODES = { API: 3, VALIDATION: 4, NOT_FOUND: 5, + CHANGES_PENDING: 10, + PARTIAL_FAILURE: 11, } as const export class DevhelmError extends Error { @@ -39,19 +39,3 @@ export class NotFoundError extends DevhelmError { this.name = 'NotFoundError' } } - -export function handleApiError(error: unknown): never { - if (error instanceof ApiRequestError) { - if (error.status === 401 || error.status === 403) { - throw new AuthError(`Authentication failed: ${error.message}`) - } - - if (error.status === 404) { - throw new DevhelmError(error.message, EXIT_CODES.NOT_FOUND) - } - - throw new DevhelmError(error.message, EXIT_CODES.API) - } - - throw error -} diff --git a/src/lib/yaml/applier.ts b/src/lib/yaml/applier.ts index 6aa1a50..76ac97b 100644 --- a/src/lib/yaml/applier.ts +++ b/src/lib/yaml/applier.ts @@ -97,7 +97,7 @@ export async function apply( for (const change of changeset.deletes) { try { const handler = lookupHandler(change.resourceType, 'delete') - await apiDelete(client, handler.deletePath(change.existingId!)) + await apiDelete(client, handler.deletePath(change.existingId!, change.refKey)) succeeded.push({action: 'delete', resourceType: change.resourceType, refKey: change.refKey}) } catch (err) { failed.push({ @@ -110,10 +110,11 @@ export async function apply( for (const change of changeset.memberships) { try { await applyMembership(change, refs, client) - succeeded.push({action: 'membership', resourceType: 'groupMembership', refKey: change.refKey}) + const icon = change.action === 'delete' ? 'remove' : 'add' + succeeded.push({action: icon, resourceType: 'groupMembership', refKey: change.refKey}) } catch (err) { failed.push({ - action: 'membership', resourceType: 'groupMembership', + action: change.action, resourceType: 'groupMembership', refKey: change.refKey, error: errorMessage(err), }) } @@ -122,14 +123,25 @@ export async function apply( return {succeeded, failed, stateEntries} } -interface MembershipPayload { +interface MembershipCreatePayload { groupName: string memberType: string memberRef: string } +interface MembershipDeletePayload { + groupId: string + memberId: string +} + async function applyMembership(change: Change, refs: ResolvedRefs, client: ApiClient): Promise { - const desired = change.desired as MembershipPayload + if (change.action === 'delete') { + const payload = change.desired as MembershipDeletePayload + await apiDelete(client, `/api/v1/resource-groups/${payload.groupId}/members/${payload.memberId}`) + return + } + + const desired = change.desired as MembershipCreatePayload const groupId = refs.require('resourceGroups', desired.groupName) const memberType = desired.memberType diff --git a/src/lib/yaml/differ.ts b/src/lib/yaml/differ.ts index 182b12d..59dd40e 100644 --- a/src/lib/yaml/differ.ts +++ b/src/lib/yaml/differ.ts @@ -5,12 +5,16 @@ * Delegates all per-resource-type semantic comparison to typed handlers * in handlers.ts — no Record anywhere in this file. */ +import type {components} from '../api.generated.js' import type {DevhelmConfig} from './schema.js' import type {ResolvedRefs} from './resolver.js' import {allHandlers, type ResourceHandler} from './handlers.js' import type {Change, Changeset, DiffOptions} from './types.js' import {RESOURCE_ORDER} from './types.js' +type ResourceGroupDto = components['schemas']['ResourceGroupDto'] +type ResourceGroupMemberDto = components['schemas']['ResourceGroupMemberDto'] + // Re-export types so existing consumers don't need to change imports export type {ChangeAction, ResourceType, Change, DiffOptions, Changeset} from './types.js' @@ -26,24 +30,7 @@ export function diff(config: DevhelmConfig, refs: ResolvedRefs, options: DiffOpt diffSection(handler, config[handler.configKey], refs, creates, updates, deletes, options) } - for (const group of config.resourceGroups ?? []) { - for (const monitorName of group.monitors ?? []) { - memberships.push({ - action: 'create', - resourceType: 'groupMembership', - refKey: `${group.name} → ${monitorName}`, - desired: {groupName: group.name, memberType: 'monitor', memberRef: monitorName}, - }) - } - for (const serviceSlug of group.services ?? []) { - memberships.push({ - action: 'create', - resourceType: 'groupMembership', - refKey: `${group.name} → ${serviceSlug}`, - desired: {groupName: group.name, memberType: 'service', memberRef: serviceSlug}, - }) - } - } + diffMemberships(config, refs, memberships, options) creates.sort((a, b) => RESOURCE_ORDER.indexOf(a.resourceType) - RESOURCE_ORDER.indexOf(b.resourceType)) updates.sort((a, b) => RESOURCE_ORDER.indexOf(a.resourceType) - RESOURCE_ORDER.indexOf(b.resourceType)) @@ -107,6 +94,118 @@ function diffSection( } } +// ── Membership diff ──────────────────────────────────────────────────── + +function memberKey(memberType: string, nameOrSlug: string): string { + return `${memberType}:${nameOrSlug}` +} + +function diffMemberships( + config: DevhelmConfig, + refs: ResolvedRefs, + memberships: Change[], + options: DiffOptions, +): void { + for (const group of config.resourceGroups ?? []) { + const groupEntry = refs.get('resourceGroups', group.name) + const currentMembers = new Map() + + if (groupEntry) { + const dto = groupEntry.raw as ResourceGroupDto + for (const m of dto.members ?? []) { + if (m.memberType === 'monitor' && m.name) { + currentMembers.set(memberKey('monitor', m.name), m) + } else if (m.memberType === 'service' && m.slug) { + currentMembers.set(memberKey('service', m.slug), m) + } + } + } + + const desired = new Set() + + for (const monitorName of group.monitors ?? []) { + const key = memberKey('monitor', monitorName) + desired.add(key) + if (!currentMembers.has(key)) { + memberships.push({ + action: 'create', + resourceType: 'groupMembership', + refKey: `${group.name} → ${monitorName}`, + desired: {groupName: group.name, memberType: 'monitor', memberRef: monitorName}, + }) + } + } + + for (const serviceSlug of group.services ?? []) { + const key = memberKey('service', serviceSlug) + desired.add(key) + if (!currentMembers.has(key)) { + memberships.push({ + action: 'create', + resourceType: 'groupMembership', + refKey: `${group.name} → ${serviceSlug}`, + desired: {groupName: group.name, memberType: 'service', memberRef: serviceSlug}, + }) + } + } + + if (options.prune && groupEntry) { + for (const [key, member] of currentMembers) { + if (!desired.has(key)) { + const label = member.name ?? member.slug ?? member.id ?? 'unknown' + memberships.push({ + action: 'delete', + resourceType: 'groupMembership', + refKey: `${group.name} → ${label}`, + existingId: member.id, + desired: {groupId: groupEntry.id, memberId: member.id}, + }) + } + } + } + } +} + +// ── JSON serialization ───────────────────────────────────────────────── + +export interface ChangesetJson { + format_version: string + creates: ChangeJson[] + updates: ChangeJson[] + deletes: ChangeJson[] + memberships: ChangeJson[] + summary: {creates: number; updates: number; deletes: number; memberships: number} +} + +interface ChangeJson { + action: string + resource_type: string + ref_key: string + existing_id?: string +} + +function changeToJson(c: Change): ChangeJson { + const out: ChangeJson = {action: c.action, resource_type: c.resourceType, ref_key: c.refKey} + if (c.existingId) out.existing_id = c.existingId + return out +} + +export function changesetToJson(changeset: Changeset): ChangesetJson { + return { + format_version: '1', + creates: changeset.creates.map(changeToJson), + updates: changeset.updates.map(changeToJson), + deletes: changeset.deletes.map(changeToJson), + memberships: changeset.memberships.map(changeToJson), + summary: { + creates: changeset.creates.length, + updates: changeset.updates.length, + deletes: changeset.deletes.length, + memberships: changeset.memberships.length, + }, + } +} + // ── Plan formatting ──────────────────────────────────────────────────── export function formatPlan(changeset: Changeset): string { @@ -129,7 +228,8 @@ export function formatPlan(changeset: Changeset): string { lines.push(` - ${c.resourceType} "${c.refKey}"`) } for (const c of changeset.memberships) { - lines.push(` → ${c.refKey}`) + const icon = c.action === 'delete' ? '- membership' : '→' + lines.push(` ${icon} ${c.refKey}`) } return lines.join('\n') diff --git a/src/lib/yaml/handlers.ts b/src/lib/yaml/handlers.ts index b9a1d4a..2f35dde 100644 --- a/src/lib/yaml/handlers.ts +++ b/src/lib/yaml/handlers.ts @@ -79,7 +79,7 @@ export interface ResourceHandler { fetchAll(client: ApiClient): Promise applyCreate(yaml: TYaml, refs: ResolvedRefs, client: ApiClient): Promise applyUpdate(yaml: TYaml, existingId: string, refs: ResolvedRefs, client: ApiClient): Promise - deletePath(id: string): string + deletePath(id: string, refKey: string): string } // ── Handler definition (snapshot-based) ───────────────────────────────── @@ -106,7 +106,7 @@ interface HandlerDef { fetchAll(client: ApiClient): Promise applyCreate(yaml: TYaml, refs: ResolvedRefs, client: ApiClient): Promise applyUpdate(yaml: TYaml, existingId: string, refs: ResolvedRefs, client: ApiClient): Promise - deletePath(id: string): string + deletePath(id: string, refKey: string): string } /** @@ -234,12 +234,12 @@ const environmentHandler = defineHandler `/api/v1/environments/${id}`, + deletePath: (_id, refKey) => `/api/v1/environments/${refKey}`, }) // ── Secret ────────────────────────────────────────────────────────────── @@ -383,7 +383,7 @@ const webhookHandler = defineHandler ({ url: api.url ?? null, @@ -399,7 +399,10 @@ const webhookHandler = defineHandler `/api/v1/webhooks/${id}`, }) diff --git a/src/lib/yaml/index.ts b/src/lib/yaml/index.ts index bfd5a98..4247a5a 100644 --- a/src/lib/yaml/index.ts +++ b/src/lib/yaml/index.ts @@ -5,8 +5,8 @@ export {validate} from './validator.js' export type {ValidationResult, ValidationError} from './validator.js' export {interpolate, findMissingVariables, InterpolationError} from './interpolation.js' export {fetchAllRefs, ResolvedRefs} from './resolver.js' -export {diff, formatPlan} from './differ.js' -export type {Changeset, Change} from './differ.js' +export {diff, formatPlan, changesetToJson} from './differ.js' +export type {Changeset, Change, ChangesetJson} from './differ.js' export {apply} from './applier.js' export type {ApplyResult} from './applier.js' export {readState, writeState, buildState} from './state.js' diff --git a/src/lib/yaml/schema.ts b/src/lib/yaml/schema.ts index c220309..1cb2ff9 100644 --- a/src/lib/yaml/schema.ts +++ b/src/lib/yaml/schema.ts @@ -298,6 +298,7 @@ export interface YamlWebhook { url: string events: string[] description?: string + enabled?: boolean } export interface YamlResourceGroup { @@ -331,7 +332,6 @@ export interface YamlMonitor { assertions?: YamlAssertion[] auth?: YamlAuth incidentPolicy?: YamlIncidentPolicy - resourceGroup?: string } export interface YamlDependency { diff --git a/src/lib/yaml/validator.ts b/src/lib/yaml/validator.ts index 54fdc51..6746d7e 100644 --- a/src/lib/yaml/validator.ts +++ b/src/lib/yaml/validator.ts @@ -324,10 +324,6 @@ function validateMonitor(monitor: YamlMonitor, path: string, ctx: ValidationCont ctx.checkRef('alertChannels', name, `${path}.alertChannels`) } } - if (monitor.resourceGroup) { - ctx.checkRef('resourceGroups', monitor.resourceGroup, `${path}.resourceGroup`) - } - if (monitor.assertions) { for (let i = 0; i < monitor.assertions.length; i++) { validateAssertionDef(monitor.assertions[i], `${path}.assertions[${i}]`, ctx) From 82b4487591087c94359649df19091b96db8e1d71 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 15:57:20 +0200 Subject: [PATCH 4/7] chore: sync OpenAPI spec, add AcquireDeployLockRequest to description extraction - Update monitoring-api.json with latest spec (includes @Schema descriptions) - Regenerate api.generated.ts and descriptions.generated.ts - Add AcquireDeployLockRequest to extract-descriptions TARGET_SCHEMAS - Add cross-reference comment pointing to monorepo MUST_HAVE list Made-with: Cursor --- docs/openapi/monitoring-api.json | 864 ++++++++++++---- scripts/extract-descriptions.mjs | 4 +- src/lib/api.generated.ts | 1546 ++++++++++++++++++++++++----- src/lib/descriptions.generated.ts | 4 + 4 files changed, 1975 insertions(+), 443 deletions(-) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index db390c8..9dd3089 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -7128,26 +7128,32 @@ "properties": { "id": { "type": "integer", + "description": "Unique billing plan identifier", "format": "int32" }, "paddleId": { - "type": "string" + "type": "string", + "description": "Paddle product identifier" }, "name": { - "type": "string" + "type": "string", + "description": "Billing plan display name" }, "description": { "type": "string", + "description": "Plan description", "nullable": true }, "prices": { "type": "array", + "description": "Available prices for this plan; null when not requested", "nullable": true, "items": { "$ref": "#/components/schemas/BillingPriceDto" } } }, + "description": "Associated billing plan; null when not requested", "nullable": true }, "BillingPriceDto": { @@ -7155,17 +7161,21 @@ "properties": { "id": { "type": "integer", + "description": "Unique billing price identifier", "format": "int32" }, "paddleId": { - "type": "string" + "type": "string", + "description": "Paddle price identifier" }, "amount": { "type": "integer", + "description": "Price amount in smallest currency unit (e.g. cents)", "format": "int32" }, "interval": { "type": "string", + "description": "Billing interval (MONTH or YEAR)", "enum": [ "DAY", "WEEK", @@ -7175,16 +7185,19 @@ }, "intervalCount": { "type": "integer", + "description": "Number of intervals between billing cycles", "format": "int32" }, "description": { "type": "string", + "description": "Price description", "nullable": true }, "billingPlan": { "$ref": "#/components/schemas/BillingPlanDto" } - } + }, + "description": "Price details for this line item" }, "ItemDto": { "type": "object", @@ -7194,13 +7207,16 @@ }, "quantity": { "type": "integer", + "description": "Quantity of this price", "format": "int32" }, "amount": { "type": "integer", + "description": "Line item total in smallest currency unit", "format": "int32" } - } + }, + "description": "Line items included in this subscription" }, "SingleValueResponseSubscriptionDto": { "type": "object", @@ -7215,25 +7231,31 @@ "properties": { "id": { "type": "integer", + "description": "Internal subscription identifier", "format": "int32" }, "paddleId": { - "type": "string" + "type": "string", + "description": "Paddle subscription identifier" }, "createdAt": { "type": "string", + "description": "Timestamp when the subscription was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the subscription was last updated", "format": "date-time" }, "organizationId": { "type": "integer", + "description": "Organization this subscription belongs to", "format": "int32" }, "status": { "type": "string", + "description": "Current subscription status", "enum": [ "ACTIVE", "CANCELED", @@ -7244,21 +7266,25 @@ }, "nextBilledAt": { "type": "string", + "description": "Next billing date; null when cancelled or expired", "format": "date-time", "nullable": true }, "willCancelAt": { "type": "string", + "description": "Scheduled cancellation date; null if no cancellation pending", "format": "date-time", "nullable": true }, "items": { "type": "array", + "description": "Line items included in this subscription", "items": { "$ref": "#/components/schemas/ItemDto" } } - } + }, + "description": "Current billing subscription details" }, "UpdateOrgDetailsRequest": { "required": [ @@ -7270,27 +7296,32 @@ "name": { "maxLength": 200, "minLength": 0, - "type": "string" + "type": "string", + "description": "New organization name (max 200 chars)" }, "email": { "minLength": 1, "type": "string", + "description": "New billing and contact email address", "format": "email" }, "size": { "maxLength": 50, "minLength": 0, - "type": "string" + "type": "string", + "description": "Team size range (e.g. 1-10, 11-50)" }, "industry": { "maxLength": 100, "minLength": 0, - "type": "string" + "type": "string", + "description": "Industry vertical (e.g. SaaS, Fintech)" }, "websiteUrl": { "maxLength": 255, "minLength": 0, - "type": "string" + "type": "string", + "description": "Organization website URL (max 255 chars)" } } }, @@ -7299,28 +7330,35 @@ "properties": { "id": { "type": "integer", + "description": "Unique organization identifier", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Organization name" }, "email": { "type": "string", + "description": "Billing and contact email", "nullable": true }, "size": { "type": "string", + "description": "Team size range (e.g. 1-10, 11-50)", "nullable": true }, "industry": { "type": "string", + "description": "Industry vertical (e.g. SaaS, Fintech)", "nullable": true }, "websiteUrl": { "type": "string", + "description": "Organization website URL", "nullable": true } - } + }, + "description": "Organization account details" }, "SingleValueResponseOrganizationDto": { "type": "object", @@ -7338,6 +7376,7 @@ "properties": { "stage": { "type": "string", + "description": "New onboarding stage", "enum": [ "WELCOME", "FIRST_MONITOR", @@ -7345,7 +7384,8 @@ "COMPLETED" ] } - } + }, + "description": "Advance the user's onboarding stage" }, "SingleValueResponseUserDto": { "type": "object", @@ -7360,20 +7400,25 @@ "properties": { "id": { "type": "integer", + "description": "Unique user identifier", "format": "int32" }, "email": { - "type": "string" + "type": "string", + "description": "User email address" }, "emailVerified": { - "type": "boolean" + "type": "boolean", + "description": "Whether the email address has been verified" }, "name": { "type": "string", + "description": "Display name; null if not set", "nullable": true }, "userRole": { "type": "string", + "description": "Platform role: USER or SUPERADMIN", "enum": [ "SUPERADMIN", "ADMIN", @@ -7382,6 +7427,7 @@ }, "onboardingStage": { "type": "string", + "description": "Current onboarding progress stage; null when completed", "nullable": true, "enum": [ "WELCOME", @@ -7392,17 +7438,21 @@ }, "imageUrl": { "type": "string", + "description": "Profile image URL; null if not set", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the account was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the account was last updated", "format": "date-time" } - } + }, + "description": "User account details" }, "UpdateProfileRequest": { "type": "object", @@ -7410,7 +7460,8 @@ "name": { "maxLength": 200, "minLength": 0, - "type": "string" + "type": "string", + "description": "New display name (max 200 chars)" } } }, @@ -7423,10 +7474,13 @@ "preferences": { "type": "object", "additionalProperties": { - "type": "boolean" - } + "type": "boolean", + "description": "Map of category keys to enabled/disabled flags" + }, + "description": "Map of category keys to enabled/disabled flags" } - } + }, + "description": "Replace notification preferences for the current user" }, "NotificationPreferencesDto": { "type": "object", @@ -7434,14 +7488,18 @@ "preferences": { "type": "object", "additionalProperties": { - "type": "boolean" - } + "type": "boolean", + "description": "Map of category keys to enabled/disabled flags" + }, + "description": "Map of category keys to enabled/disabled flags" }, "updatedAt": { "type": "string", + "description": "Timestamp when preferences were last updated", "format": "date-time" } - } + }, + "description": "User notification preferences keyed by notification category" }, "SingleValueResponseNotificationPreferencesDto": { "type": "object", @@ -7460,9 +7518,11 @@ "name": { "maxLength": 200, "minLength": 0, - "type": "string" + "type": "string", + "description": "New workspace name" } - } + }, + "description": "Update workspace details" }, "SingleValueResponseWorkspaceDto": { "type": "object", @@ -7477,24 +7537,30 @@ "properties": { "id": { "type": "integer", + "description": "Unique workspace identifier", "format": "int32" }, "createdAt": { "type": "string", + "description": "Timestamp when the workspace was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the workspace was last updated", "format": "date-time" }, "name": { - "type": "string" + "type": "string", + "description": "Workspace name" }, "orgId": { "type": "integer", + "description": "Organization this workspace belongs to", "format": "int32" } - } + }, + "description": "Workspace within an organization" }, "UpdateUserRequest": { "type": "object", @@ -7502,14 +7568,17 @@ "name": { "maxLength": 200, "minLength": 0, - "type": "string" + "type": "string", + "description": "New display name (max 200 chars)" }, "email": { "type": "string", + "description": "New email address", "format": "email" }, "userRole": { "type": "string", + "description": "New platform role", "enum": [ "SUPERADMIN", "ADMIN", @@ -7518,6 +7587,7 @@ }, "onboardingStage": { "type": "string", + "description": "New onboarding stage", "enum": [ "WELCOME", "FIRST_MONITOR", @@ -7528,7 +7598,8 @@ "imageUrl": { "maxLength": 500, "minLength": 0, - "type": "string" + "type": "string", + "description": "New profile image URL (max 500 chars)" } } }, @@ -7540,13 +7611,15 @@ "properties": { "orgRole": { "type": "string", + "description": "New role to assign", "enum": [ "OWNER", "ADMIN", "MEMBER" ] } - } + }, + "description": "Update an organization member's role" }, "UpdateWebhookEndpointRequest": { "type": "object", @@ -7595,46 +7668,58 @@ "properties": { "id": { "type": "string", + "description": "Unique webhook endpoint identifier", "format": "uuid" }, "url": { - "type": "string" + "type": "string", + "description": "HTTPS endpoint URL that receives event payloads" }, "description": { "type": "string", + "description": "Human-readable description of this endpoint", "nullable": true }, "subscribedEvents": { "type": "array", + "description": "Event types this endpoint is subscribed to", "items": { - "type": "string" + "type": "string", + "description": "Event types this endpoint is subscribed to" } }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Whether delivery is enabled for this endpoint" }, "consecutiveFailures": { "type": "integer", + "description": "Number of consecutive delivery failures", "format": "int32" }, "disabledReason": { "type": "string", + "description": "Reason the endpoint was auto-disabled", "nullable": true }, "disabledAt": { "type": "string", + "description": "Timestamp when the endpoint was auto-disabled", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the endpoint was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the endpoint was last updated", "format": "date-time" } - } + }, + "description": "Webhook endpoint that receives event delivery payloads" }, "UpdateTagRequest": { "type": "object", @@ -7668,27 +7753,34 @@ "properties": { "id": { "type": "string", + "description": "Unique tag identifier", "format": "uuid" }, "organizationId": { "type": "integer", + "description": "Organization this tag belongs to", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Tag name, unique within the org" }, "color": { - "type": "string" + "type": "string", + "description": "Hex color code for display (e.g. #6B7280)" }, "createdAt": { "type": "string", + "description": "Timestamp when the tag was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the tag was last updated", "format": "date-time" } - } + }, + "description": "Tag for organizing and filtering monitors" }, "UpdateSecretRequest": { "required": [ @@ -7709,46 +7801,57 @@ "properties": { "id": { "type": "string", + "description": "Monitor identifier", "format": "uuid" }, "name": { - "type": "string" + "type": "string", + "description": "Monitor name" } - } + }, + "description": "Monitors that reference this secret; null on create/update responses" }, "SecretDto": { "type": "object", "properties": { "id": { "type": "string", + "description": "Unique secret identifier", "format": "uuid" }, "key": { - "type": "string" + "type": "string", + "description": "Secret key name, unique within the workspace" }, "dekVersion": { "type": "integer", + "description": "DEK version at the time of last encryption", "format": "int32" }, "valueHash": { - "type": "string" + "type": "string", + "description": "SHA-256 hex digest of the current plaintext; use for change detection" }, "createdAt": { "type": "string", + "description": "Timestamp when the secret was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the secret was last updated", "format": "date-time" }, "usedByMonitors": { "type": "array", + "description": "Monitors that reference this secret; null on create/update responses", "nullable": true, "items": { "$ref": "#/components/schemas/MonitorReference" } } - } + }, + "description": "Secret with change-detection hash; plaintext value is never returned" }, "SingleValueResponseSecretDto": { "type": "object", @@ -7765,14 +7868,17 @@ "type": "object", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Retry strategy kind, e.g. fixed interval between attempts" }, "maxRetries": { "type": "integer", + "description": "Maximum number of retries after a failed check", "format": "int32" }, "interval": { "type": "integer", + "description": "Delay between retry attempts in seconds", "format": "int32" } }, @@ -7886,20 +7992,25 @@ "properties": { "id": { "type": "string", + "description": "Unique resource group identifier", "format": "uuid" }, "organizationId": { "type": "integer", + "description": "Organization this group belongs to", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable group name" }, "slug": { - "type": "string" + "type": "string", + "description": "URL-safe group identifier" }, "description": { "type": "string", + "description": "Optional group description", "nullable": true }, "alertPolicyId": { @@ -7987,10 +8098,12 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the group was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the group was last updated", "format": "date-time" } }, @@ -8048,10 +8161,12 @@ "properties": { "id": { "type": "string", + "description": "Unique group member record identifier", "format": "uuid" }, "groupId": { "type": "string", + "description": "Resource group this member belongs to", "format": "uuid" }, "memberType": { @@ -8103,6 +8218,7 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the member was added to the group", "format": "date-time" }, "uptime24h": { @@ -8316,10 +8432,12 @@ "properties": { "id": { "type": "string", + "description": "Unique notification policy identifier", "format": "uuid" }, "organizationId": { "type": "integer", + "description": "Organization this policy belongs to", "format": "int32" }, "name": { @@ -8347,10 +8465,12 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the policy was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the policy was last updated", "format": "date-time" } }, @@ -8372,16 +8492,19 @@ "properties": { "type": { "type": "string", + "description": "How incident confirmation is coordinated across regions", "enum": [ "multi_region" ] }, "minRegionsFailing": { "type": "integer", + "description": "Minimum failing regions required to confirm an incident", "format": "int32" }, "maxWaitSeconds": { "type": "integer", + "description": "Maximum seconds to wait for enough regions to fail after first trigger", "format": "int32" } }, @@ -8392,10 +8515,12 @@ "properties": { "id": { "type": "string", + "description": "Unique incident policy identifier", "format": "uuid" }, "monitorId": { "type": "string", + "description": "Monitor this policy is attached to", "format": "uuid" }, "triggerRules": { @@ -8413,10 +8538,12 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the policy was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the policy was last updated", "format": "date-time" }, "monitorRegionCount": { @@ -8439,14 +8566,17 @@ "properties": { "consecutiveSuccesses": { "type": "integer", + "description": "Consecutive passing checks required to auto-resolve the incident", "format": "int32" }, "minRegionsPassing": { "type": "integer", + "description": "Minimum regions that must be passing before recovery can complete", "format": "int32" }, "cooldownMinutes": { "type": "integer", + "description": "Minutes after resolve before a new incident may open on the same monitor", "format": "int32" } }, @@ -8462,6 +8592,7 @@ "properties": { "type": { "type": "string", + "description": "Condition that opens or escalates an incident from check results", "enum": [ "consecutive_failures", "failures_in_window", @@ -8470,16 +8601,19 @@ }, "count": { "type": "integer", + "description": "Failure count for consecutive or windowed failure rules", "format": "int32", "nullable": true }, "windowMinutes": { "type": "integer", + "description": "Window length in minutes for failures-in-window rules", "format": "int32", "nullable": true }, "scope": { "type": "string", + "description": "Whether the rule applies per region or across regions", "nullable": true, "enum": [ "per_region", @@ -8488,11 +8622,13 @@ }, "thresholdMs": { "type": "integer", + "description": "Response time threshold in milliseconds for response-time rules", "format": "int32", "nullable": true }, "severity": { "type": "string", + "description": "Incident severity when this rule fires", "enum": [ "down", "degraded" @@ -8500,6 +8636,7 @@ }, "aggregationType": { "type": "string", + "description": "How response times are aggregated for response-time rules", "nullable": true, "enum": [ "all_exceed", @@ -8559,10 +8696,12 @@ "headerName": { "minLength": 1, "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string" + "type": "string", + "description": "HTTP header name that carries the API key" }, "vaultSecretId": { "type": "string", + "description": "Vault secret ID for the API key value", "format": "uuid", "nullable": true } @@ -8581,6 +8720,7 @@ "properties": { "vaultSecretId": { "type": "string", + "description": "Vault secret ID holding Basic auth username and password", "format": "uuid", "nullable": true } @@ -8599,6 +8739,7 @@ "properties": { "vaultSecretId": { "type": "string", + "description": "Vault secret ID holding the bearer token value", "format": "uuid", "nullable": true } @@ -8621,10 +8762,12 @@ "headerName": { "minLength": 1, "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string" + "type": "string", + "description": "Custom HTTP header name for the secret value" }, "vaultSecretId": { "type": "string", + "description": "Vault secret ID for the header value", "format": "uuid", "nullable": true } @@ -8642,6 +8785,7 @@ "type": "string" } }, + "description": "New authentication configuration (full replacement)", "discriminator": { "propertyName": "type" } @@ -8726,6 +8870,7 @@ "type": "string" } }, + "description": "New assertion configuration (full replacement)", "discriminator": { "propertyName": "type" } @@ -8744,7 +8889,8 @@ "properties": { "substring": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Substring that must appear in the response body" } } } @@ -8764,7 +8910,8 @@ "properties": { "value": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected CNAME target the resolution must include" } } } @@ -8785,8 +8932,10 @@ "ips": { "minItems": 1, "type": "array", + "description": "Allowed IP addresses; at least one resolved address must match", "items": { - "type": "string" + "type": "string", + "description": "Allowed IP addresses; at least one resolved address must match" } } } @@ -8807,10 +8956,12 @@ "properties": { "recordType": { "minLength": 1, - "type": "string" + "type": "string", + "description": "DNS record type whose answer count is checked" }, "max": { "type": "integer", + "description": "Maximum number of answers allowed for that record type", "format": "int32" } } @@ -8831,10 +8982,12 @@ "properties": { "recordType": { "minLength": 1, - "type": "string" + "type": "string", + "description": "DNS record type whose answer count is checked" }, "min": { "type": "integer", + "description": "Minimum number of answers required for that record type", "format": "int32" } } @@ -8856,11 +9009,13 @@ "properties": { "recordType": { "minLength": 1, - "type": "string" + "type": "string", + "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" }, "substring": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Substring that must appear in a matching record value" } } } @@ -8881,11 +9036,13 @@ "properties": { "recordType": { "minLength": 1, - "type": "string" + "type": "string", + "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" }, "value": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected DNS record value for an exact match" } } } @@ -8910,6 +9067,7 @@ "properties": { "maxMs": { "type": "integer", + "description": "Maximum allowed DNS resolution time in milliseconds", "format": "int32" } } @@ -8927,6 +9085,7 @@ "properties": { "warnMs": { "type": "integer", + "description": "DNS resolution time in milliseconds that triggers a warning only", "format": "int32" } } @@ -8944,6 +9103,7 @@ "properties": { "maxTtl": { "type": "integer", + "description": "Maximum TTL in seconds before a high-TTL warning is raised", "format": "int32" } } @@ -8961,6 +9121,7 @@ "properties": { "minTtl": { "type": "integer", + "description": "Minimum acceptable TTL in seconds before a warning is raised", "format": "int32" } } @@ -8981,7 +9142,8 @@ "properties": { "substring": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Substring that must appear in at least one TXT record" } } } @@ -9003,14 +9165,17 @@ "properties": { "headerName": { "minLength": 1, - "type": "string" + "type": "string", + "description": "HTTP header name to assert on" }, "expected": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected value to compare against" }, "operator": { "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", "enum": [ "equals", "contains", @@ -9040,6 +9205,7 @@ "maximum": 100, "minimum": 1, "type": "integer", + "description": "Max percent drift from expected ping interval before warning (non-fatal)", "format": "int32" } } @@ -9061,6 +9227,7 @@ "maxSeconds": { "minimum": 1, "type": "integer", + "description": "Maximum allowed gap in seconds between consecutive heartbeat pings", "format": "int32" } } @@ -9082,10 +9249,12 @@ "properties": { "path": { "minLength": 1, - "type": "string" + "type": "string", + "description": "JSONPath expression into the heartbeat ping JSON payload" }, "value": { - "type": "string" + "type": "string", + "description": "Expected value to compare against at that path" } } } @@ -9114,6 +9283,7 @@ "minimum": 0.0, "exclusiveMinimum": false, "type": "number", + "description": "Maximum allowed packet loss percentage before the check fails (0\u2013100)", "format": "double" } } @@ -9139,6 +9309,7 @@ "properties": { "maxMs": { "type": "integer", + "description": "Maximum average ICMP round-trip time in milliseconds", "format": "int32" } } @@ -9156,6 +9327,7 @@ "properties": { "warnMs": { "type": "integer", + "description": "ICMP round-trip time in milliseconds that triggers a warning only", "format": "int32" } } @@ -9178,14 +9350,17 @@ "properties": { "path": { "minLength": 1, - "type": "string" + "type": "string", + "description": "JSONPath expression to extract a value from the response body" }, "expected": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected value to compare against" }, "operator": { "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", "enum": [ "equals", "contains", @@ -9221,7 +9396,8 @@ "properties": { "capability": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Capability name the server must advertise, e.g. tools or resources" } } } @@ -9238,6 +9414,7 @@ "properties": { "min": { "type": "integer", + "description": "Minimum number of tools the server must expose", "format": "int32" } } @@ -9258,7 +9435,8 @@ "properties": { "version": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected MCP protocol version string from the server handshake" } } } @@ -9275,6 +9453,7 @@ "properties": { "maxMs": { "type": "integer", + "description": "Maximum allowed MCP check duration in milliseconds", "format": "int32" } } @@ -9292,6 +9471,7 @@ "properties": { "warnMs": { "type": "integer", + "description": "MCP check duration in milliseconds that triggers a warning only", "format": "int32" } } @@ -9312,7 +9492,8 @@ "properties": { "toolName": { "minLength": 1, - "type": "string" + "type": "string", + "description": "MCP tool name that must appear in the server's tool list" } } } @@ -9329,6 +9510,7 @@ "properties": { "expectedCount": { "type": "integer", + "description": "Expected tool count; warns when the live count differs", "format": "int32" } } @@ -9346,6 +9528,7 @@ "properties": { "maxCount": { "type": "integer", + "description": "Maximum number of HTTP redirects allowed before the check fails", "format": "int32" } } @@ -9367,10 +9550,12 @@ "properties": { "expected": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected final URL after following redirects" }, "operator": { "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", "enum": [ "equals", "contains", @@ -9398,7 +9583,8 @@ "properties": { "pattern": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Regular expression the response body must match" } } } @@ -9415,6 +9601,7 @@ "properties": { "maxBytes": { "type": "integer", + "description": "Maximum response body size in bytes before the check fails", "format": "int32" } } @@ -9432,6 +9619,7 @@ "properties": { "thresholdMs": { "type": "integer", + "description": "Maximum allowed response time in milliseconds before the check fails", "format": "int32" } } @@ -9449,6 +9637,7 @@ "properties": { "warnMs": { "type": "integer", + "description": "HTTP response time in milliseconds that triggers a warning only", "format": "int32" } } @@ -9466,6 +9655,7 @@ "properties": { "minDaysRemaining": { "type": "integer", + "description": "Minimum days before TLS certificate expiry; fails or warns below this threshold", "format": "int32" } } @@ -9487,10 +9677,12 @@ "properties": { "expected": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Expected status code, range pattern, or wildcard such as 2xx" }, "operator": { "type": "string", + "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", "enum": [ "equals", "contains", @@ -9523,6 +9715,7 @@ "properties": { "maxMs": { "type": "integer", + "description": "Maximum TCP connect time in milliseconds before the check fails", "format": "int32" } } @@ -9540,6 +9733,7 @@ "properties": { "warnMs": { "type": "integer", + "description": "TCP connect time in milliseconds that triggers a warning only", "format": "int32" } } @@ -9684,6 +9878,7 @@ }, "severity": { "type": "string", + "description": "New outcome severity: FAIL or WARN", "enum": [ "fail", "warn" @@ -9904,21 +10099,26 @@ "properties": { "channelIds": { "type": "array", + "description": "IDs of alert channels to link (replaces current list)", "items": { "type": "string", + "description": "IDs of alert channels to link (replaces current list)", "format": "uuid" } } - } + }, + "description": "Replace the alert channels linked to a monitor" }, "SingleValueResponseListUUID": { "type": "object", "properties": { "data": { "type": "array", + "nullable": true, "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "nullable": true } } } @@ -10086,6 +10286,7 @@ }, "severity": { "type": "string", + "description": "Outcome severity: FAIL (fails the check) or WARN (warns without failing)", "enum": [ "fail", "warn" @@ -10509,14 +10710,17 @@ "properties": { "id": { "type": "string", + "description": "Unique monitor identifier", "format": "uuid" }, "organizationId": { "type": "integer", + "description": "Organization this monitor belongs to", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable name for this monitor" }, "type": { "type": "string", @@ -10553,19 +10757,24 @@ }, "frequencySeconds": { "type": "integer", + "description": "Check frequency in seconds (30\u201386400)", "format": "int32" }, "enabled": { - "type": "boolean" + "type": "boolean", + "description": "Whether the monitor is active" }, "regions": { "type": "array", + "description": "Probe regions where checks are executed", "items": { - "type": "string" + "type": "string", + "description": "Probe regions where checks are executed" } }, "managedBy": { "type": "string", + "description": "Management source: DASHBOARD or CLI", "enum": [ "DASHBOARD", "CLI" @@ -10573,14 +10782,17 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the monitor was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the monitor was last updated", "format": "date-time" }, "assertions": { "type": "array", + "description": "Assertions evaluated against each check result; null on list responses", "nullable": true, "items": { "$ref": "#/components/schemas/MonitorAssertionDto" @@ -10588,6 +10800,7 @@ }, "tags": { "type": "array", + "description": "Tags applied to this monitor", "nullable": true, "items": { "$ref": "#/components/schemas/TagDto" @@ -10595,6 +10808,7 @@ }, "pingUrl": { "type": "string", + "description": "Heartbeat ping URL; populated for HEARTBEAT monitors only", "nullable": true }, "environment": { @@ -10621,14 +10835,17 @@ }, "alertChannelIds": { "type": "array", + "description": "Alert channel IDs linked to this monitor; populated on single-monitor responses", "nullable": true, "items": { "type": "string", + "description": "Alert channel IDs linked to this monitor; populated on single-monitor responses", "format": "uuid", "nullable": true } } - } + }, + "description": "Full monitor representation" }, "SingleValueResponseMonitorDto": { "type": "object", @@ -10651,7 +10868,8 @@ "slug": { "type": "string" } - } + }, + "description": "Environment associated with this monitor; null when unassigned" }, "ChangeStatusRequest": { "required": [ @@ -10661,6 +10879,7 @@ "properties": { "status": { "type": "string", + "description": "New membership status (ACTIVE or SUSPENDED)", "enum": [ "INVITED", "ACTIVE", @@ -10670,7 +10889,8 @@ "DECLINED" ] } - } + }, + "description": "Update an organization member's status" }, "UpdateMaintenanceWindowRequest": { "required": [ @@ -10681,26 +10901,32 @@ "properties": { "monitorId": { "type": "string", + "description": "Monitor to attach this maintenance window to; null preserves current", "format": "uuid" }, "startsAt": { "type": "string", + "description": "Updated start time (ISO 8601)", "format": "date-time" }, "endsAt": { "type": "string", + "description": "Updated end time (ISO 8601)", "format": "date-time" }, "repeatRule": { "maxLength": 100, "minLength": 0, - "type": "string" + "type": "string", + "description": "Updated iCal RRULE; null clears the repeat rule" }, "reason": { - "type": "string" + "type": "string", + "description": "Updated reason; null clears the existing reason" }, "suppressAlerts": { - "type": "boolean" + "type": "boolean", + "description": "Whether to suppress alerts; null preserves current" } } }, @@ -10709,41 +10935,51 @@ "properties": { "id": { "type": "string", + "description": "Unique maintenance window identifier", "format": "uuid" }, "monitorId": { "type": "string", + "description": "Monitor this window applies to; null for org-wide windows", "format": "uuid", "nullable": true }, "organizationId": { "type": "integer", + "description": "Organization this maintenance window belongs to", "format": "int32" }, "startsAt": { "type": "string", + "description": "Scheduled start of the maintenance window", "format": "date-time" }, "endsAt": { "type": "string", + "description": "Scheduled end of the maintenance window", "format": "date-time" }, "repeatRule": { "type": "string", + "description": "iCal RRULE for recurring windows; null for one-time", "nullable": true }, "reason": { "type": "string", + "description": "Human-readable reason for the maintenance", "nullable": true }, "suppressAlerts": { - "type": "boolean" + "type": "boolean", + "description": "Whether alerts are suppressed during this window" }, "createdAt": { "type": "string", + "description": "Timestamp when the window was created", "format": "date-time" } - } + }, + "description": "Scheduled maintenance window for a monitor" }, "SingleValueResponseMaintenanceWindowDto": { "type": "object", @@ -10785,40 +11021,51 @@ "properties": { "id": { "type": "string", + "description": "Unique environment identifier", "format": "uuid" }, "orgId": { "type": "integer", + "description": "Organization this environment belongs to", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable environment name" }, "slug": { - "type": "string" + "type": "string", + "description": "URL-safe identifier" }, "variables": { "type": "object", "additionalProperties": { - "type": "string" - } + "type": "string", + "description": "Key-value variable pairs available for interpolation" + }, + "description": "Key-value variable pairs available for interpolation" }, "createdAt": { "type": "string", + "description": "Timestamp when the environment was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the environment was last updated", "format": "date-time" }, "monitorCount": { "type": "integer", + "description": "Number of monitors using this environment", "format": "int32" }, "isDefault": { - "type": "boolean" + "type": "boolean", + "description": "Whether this is the default environment for new monitors" } - } + }, + "description": "Environment with variable substitutions for monitor configs" }, "SingleValueResponseEnvironmentDto": { "type": "object", @@ -11082,13 +11329,16 @@ "properties": { "id": { "type": "string", + "description": "Unique alert channel identifier", "format": "uuid" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable channel name" }, "channelType": { "type": "string", + "description": "Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL)", "enum": [ "email", "webhook", @@ -11103,32 +11353,40 @@ "type": "object", "additionalProperties": { "type": "object", + "description": "Non-sensitive display metadata; null for older channels", "nullable": true }, + "description": "Non-sensitive display metadata; null for older channels", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the channel was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the channel was last updated", "format": "date-time" }, "configHash": { "type": "string", + "description": "SHA-256 hash of the channel config; use for change detection", "nullable": true }, "lastDeliveryAt": { "type": "string", + "description": "Timestamp of the most recent delivery attempt", "format": "date-time", "nullable": true }, "lastDeliveryStatus": { "type": "string", + "description": "Outcome of the most recent delivery (SUCCESS, FAILED, etc.)", "nullable": true } - } + }, + "description": "Alert channel with non-sensitive configuration metadata" }, "SingleValueResponseAlertChannelDto": { "type": "object", @@ -11211,19 +11469,23 @@ "properties": { "id": { "type": "string", + "description": "Unique incident identifier", "format": "uuid" }, "monitorId": { "type": "string", + "description": "Monitor that triggered the incident; null for service or manual incidents", "format": "uuid", "nullable": true }, "organizationId": { "type": "integer", + "description": "Organization this incident belongs to", "format": "int32" }, "source": { "type": "string", + "description": "Incident origin: MONITOR, SERVICE, or MANUAL", "enum": [ "AUTOMATIC", "MANUAL", @@ -11234,6 +11496,7 @@ }, "status": { "type": "string", + "description": "Current lifecycle status (OPEN, RESOLVED, etc.)", "enum": [ "WATCHING", "TRIGGERED", @@ -11243,6 +11506,7 @@ }, "severity": { "type": "string", + "description": "Severity level: DOWN, DEGRADED, or MAINTENANCE", "enum": [ "DOWN", "DEGRADED", @@ -11251,58 +11515,72 @@ }, "title": { "type": "string", + "description": "Short summary of the incident; null for auto-generated incidents", "nullable": true }, "triggeredByRule": { "type": "string", + "description": "Human-readable description of the trigger rule that fired", "nullable": true }, "affectedRegions": { "type": "array", + "description": "Probe regions that observed the failure", "items": { - "type": "string" + "type": "string", + "description": "Probe regions that observed the failure" } }, "reopenCount": { "type": "integer", + "description": "Number of times this incident has been reopened", "format": "int32" }, "createdByUserId": { "type": "integer", + "description": "User who created the incident (manual incidents only)", "format": "int32", "nullable": true }, "statusPageVisible": { - "type": "boolean" + "type": "boolean", + "description": "Whether this incident is visible on the status page" }, "serviceIncidentId": { "type": "string", + "description": "Linked vendor service incident ID; null for monitor incidents", "format": "uuid", "nullable": true }, "serviceId": { "type": "string", + "description": "Linked service catalog ID; null for monitor incidents", "format": "uuid", "nullable": true }, "externalRef": { "type": "string", + "description": "External reference ID (e.g. PagerDuty incident ID)", "nullable": true }, "affectedComponents": { "type": "array", + "description": "Service components affected by this incident", "nullable": true, "items": { "type": "string", + "description": "Service components affected by this incident", "nullable": true } }, "shortlink": { "type": "string", + "description": "Short URL linking to the incident details", "nullable": true }, "resolutionReason": { "type": "string", + "description": "How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.)", "nullable": true, "enum": [ "MANUAL", @@ -11312,58 +11590,71 @@ }, "startedAt": { "type": "string", + "description": "Timestamp when the incident was detected or created", "format": "date-time", "nullable": true }, "confirmedAt": { "type": "string", + "description": "Timestamp when the incident was confirmed (multi-region confirmation)", "format": "date-time", "nullable": true }, "resolvedAt": { "type": "string", + "description": "Timestamp when the incident was resolved", "format": "date-time", "nullable": true }, "cooldownUntil": { "type": "string", + "description": "Cooldown window end; new incidents suppressed until this time", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the incident record was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the incident was last updated", "format": "date-time" }, "monitorName": { "type": "string", + "description": "Name of the associated monitor; populated on list responses", "nullable": true }, "serviceName": { "type": "string", + "description": "Name of the associated service; populated on list responses", "nullable": true }, "serviceSlug": { "type": "string", + "description": "Slug of the associated service; populated on list responses", "nullable": true }, "monitorType": { "type": "string", + "description": "Type of the associated monitor; populated on list responses", "nullable": true }, "resourceGroupId": { "type": "string", + "description": "Resource group that owns this incident; null when not group-managed", "format": "uuid", "nullable": true }, "resourceGroupName": { "type": "string", + "description": "Name of the resource group; populated on list responses", "nullable": true } - } + }, + "description": "Incident triggered by a monitor check failure or manual creation" }, "TableValueResultIncidentDto": { "type": "object", @@ -11387,7 +11678,8 @@ "properties": { "data": { "type": "integer", - "format": "int32" + "format": "int32", + "nullable": true } } }, @@ -11472,41 +11764,51 @@ "properties": { "serviceId": { "type": "string", + "description": "Service this health record belongs to", "format": "uuid" }, "serviceSlug": { - "type": "string" + "type": "string", + "description": "URL-safe service identifier" }, "serviceName": { - "type": "string" + "type": "string", + "description": "Service name" }, "adapterType": { "type": "string", + "description": "Data source adapter type", "nullable": true }, "lastSuccessAt": { "type": "string", + "description": "Timestamp of the last successful poll", "format": "date-time", "nullable": true }, "lastFailureAt": { "type": "string", + "description": "Timestamp of the last failed poll", "format": "date-time", "nullable": true }, "consecutiveFailures": { "type": "integer", + "description": "Number of consecutive poll failures", "format": "int32" }, "lastErrorMessage": { "type": "string", + "description": "Error message from the most recent failure", "nullable": true }, "disabledByHealth": { - "type": "boolean" + "type": "boolean", + "description": "Whether the adapter is disabled due to repeated failures" }, "updatedAt": { "type": "string", + "description": "Timestamp when this health record was last updated", "format": "date-time" } } @@ -11527,14 +11829,17 @@ "properties": { "name": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Organization name" }, "email": { "type": "string", + "description": "Billing and contact email address", "format": "email", "nullable": true } - } + }, + "description": "Create a new organization" }, "SingleValueResponseTransactionDto": { "type": "object", @@ -11548,46 +11853,57 @@ "type": "object", "properties": { "id": { - "type": "string" + "type": "string", + "description": "Paddle transaction identifier" }, "status": { "type": "string", + "description": "Transaction status (e.g. completed, pending)", "nullable": true }, "currencyCode": { "type": "string", + "description": "ISO 4217 currency code", "nullable": true }, "invoiceNumber": { "type": "string", + "description": "Invoice number; null if not invoiced", "nullable": true }, "billedAt": { "type": "string", + "description": "Timestamp when the transaction was billed", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the transaction was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the transaction was last updated", "format": "date-time" }, "total": { "type": "string", + "description": "Total amount as a decimal string (including tax)", "nullable": true }, "subtotal": { "type": "string", + "description": "Subtotal before tax as a decimal string", "nullable": true }, "tax": { "type": "string", + "description": "Tax amount as a decimal string", "nullable": true } - } + }, + "description": "A billing transaction from Paddle" }, "QuickMonitorRequest": { "required": [ @@ -11597,18 +11913,22 @@ "properties": { "url": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Target URL to monitor" }, "name": { "type": "string", + "description": "Human-readable monitor name; defaults to the hostname if omitted", "nullable": true }, "frequencySeconds": { "type": "integer", + "description": "Check frequency in seconds (30\u201386400); defaults to 60", "format": "int32", "nullable": true } - } + }, + "description": "Minimal request for creating an HTTP monitor quickly" }, "OnboardingSetupRequest": { "required": [ @@ -11619,18 +11939,21 @@ "name": { "maxLength": 200, "minLength": 0, - "type": "string" + "type": "string", + "description": "Organization or team name (max 200 chars)" }, "role": { "maxLength": 50, "minLength": 0, "type": "string", + "description": "User's role or job title", "nullable": true }, "teamSize": { "maxLength": 50, "minLength": 0, "type": "string", + "description": "Team size range (e.g. 1-10, 11-50)", "nullable": true } } @@ -11643,52 +11966,64 @@ "properties": { "url": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Target URL to analyze (must be a valid HTTP/HTTPS URL)" } - } + }, + "description": "URL to analyze for monitor setup suggestions" }, "AnalyzeUrlResponse": { "type": "object", "properties": { "reachable": { - "type": "boolean" + "type": "boolean", + "description": "Whether the URL responded during analysis" }, "responseTimeMs": { "type": "integer", + "description": "Response time observed during analysis in milliseconds", "format": "int64" }, "statusCode": { "type": "integer", + "description": "HTTP status code from the analysis request", "format": "int32" }, "tlsExpiry": { "type": "string", + "description": "TLS certificate expiry date; null for non-HTTPS or unavailable", "format": "date-time", "nullable": true }, "tlsDaysRemaining": { "type": "integer", + "description": "Days until TLS certificate expires; null if not applicable", "format": "int32", "nullable": true }, "contentType": { "type": "string", + "description": "Response Content-Type header value", "nullable": true }, "suggestedName": { - "type": "string" + "type": "string", + "description": "Suggested monitor name derived from the URL hostname" }, "suggestedAssertions": { "type": "array", + "description": "Recommended assertions based on the URL response", "items": { "$ref": "#/components/schemas/SuggestedAssertion" } }, "suggestedFrequencySeconds": { "type": "integer", + "description": "Suggested check frequency in seconds based on the URL", "format": "int32" } - } + }, + "description": "Analysis of a URL with monitor setup suggestions" }, "SingleValueResponseAnalyzeUrlResponse": { "type": "object", @@ -11702,15 +12037,19 @@ "type": "object", "properties": { "type": { - "type": "string" + "type": "string", + "description": "Assertion type (e.g. status_code, response_time)" }, "operator": { - "type": "string" + "type": "string", + "description": "Comparison operator (e.g. equals, less_than)" }, "value": { - "type": "string" + "type": "string", + "description": "Expected value to compare against" } - } + }, + "description": "Recommended assertions based on the URL response" }, "AcceptInviteRequest": { "required": [ @@ -11720,23 +12059,28 @@ "properties": { "token": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Invite token from the invitation email" } - } + }, + "description": "Accept an organization invite using the invite token" }, "AcceptInviteDto": { "type": "object", "properties": { "orgId": { "type": "integer", + "description": "Organization the user joined", "format": "int32" }, "userId": { "type": "integer", + "description": "User who accepted the invite", "format": "int32" }, "orgRole": { "type": "string", + "description": "Role assigned to the new member", "enum": [ "OWNER", "ADMIN", @@ -11745,6 +12089,7 @@ }, "status": { "type": "string", + "description": "Initial membership status after joining", "enum": [ "INVITED", "ACTIVE", @@ -11754,7 +12099,8 @@ "DECLINED" ] } - } + }, + "description": "Result of accepting an organization invite" }, "SingleValueResponseAcceptInviteDto": { "type": "object", @@ -11769,14 +12115,17 @@ "properties": { "nickname": { "type": "string", + "description": "User nickname from the identity provider", "nullable": true }, "name": { "type": "string", + "description": "User display name from the identity provider", "nullable": true }, "picture": { "type": "string", + "description": "Profile picture URL from the identity provider", "nullable": true } } @@ -11789,9 +12138,11 @@ "properties": { "name": { "minLength": 1, - "type": "string" + "type": "string", + "description": "Workspace name" } - } + }, + "description": "Create a new workspace within the organization" }, "AddMemberRequest": { "required": [ @@ -11802,34 +12153,41 @@ "properties": { "userId": { "type": "integer", + "description": "ID of the user to add", "format": "int32" }, "orgRole": { "type": "string", + "description": "Role to assign to the new member", "enum": [ "OWNER", "ADMIN", "MEMBER" ] } - } + }, + "description": "Add an existing user as a member of the organization" }, "MemberDto": { "type": "object", "properties": { "userId": { "type": "integer", + "description": "User identifier of the member", "format": "int32" }, "email": { - "type": "string" + "type": "string", + "description": "Member email address" }, "name": { "type": "string", + "description": "Member display name; null if not set", "nullable": true }, "orgRole": { "type": "string", + "description": "Member role within this organization (OWNER, ADMIN, MEMBER)", "enum": [ "OWNER", "ADMIN", @@ -11838,6 +12196,7 @@ }, "status": { "type": "string", + "description": "Membership status (ACTIVE, PENDING, SUSPENDED)", "enum": [ "INVITED", "ACTIVE", @@ -11849,9 +12208,11 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the member was added to the organization", "format": "date-time" } - } + }, + "description": "Organization member with role and status" }, "SingleValueResponseMemberDto": { "type": "object", @@ -11897,9 +12258,11 @@ "properties": { "eventType": { "type": "string", + "description": "Event type to simulate (e.g. monitor.created); null uses a default", "nullable": true } - } + }, + "description": "Event type to use for a test webhook delivery" }, "SingleValueResponseWebhookTestResult": { "type": "object", @@ -11934,7 +12297,8 @@ "type": "object", "properties": { "data": { - "type": "string" + "type": "string", + "nullable": true } } }, @@ -11943,25 +12307,31 @@ "properties": { "previousDekVersion": { "type": "integer", + "description": "DEK version before rotation", "format": "int32" }, "newDekVersion": { "type": "integer", + "description": "DEK version after rotation", "format": "int32" }, "secretsReEncrypted": { "type": "integer", + "description": "Number of secrets re-encrypted with the new DEK", "format": "int32" }, "channelsReEncrypted": { "type": "integer", + "description": "Number of alert channels re-encrypted with the new DEK", "format": "int32" }, "rotatedAt": { "type": "string", + "description": "Timestamp when the rotation was performed", "format": "date-time" } - } + }, + "description": "Result of a data encryption key rotation operation" }, "SingleValueResponseDekRotationResultDto": { "type": "object", @@ -12596,14 +12966,17 @@ "properties": { "id": { "type": "string", + "description": "Unique dispatch record identifier", "format": "uuid" }, "incidentId": { "type": "string", + "description": "Incident this dispatch is for", "format": "uuid" }, "policyId": { "type": "string", + "description": "Notification policy that matched this incident", "format": "uuid" }, "policyName": { @@ -12671,10 +13044,12 @@ }, "createdAt": { "type": "string", + "description": "Timestamp when the dispatch was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the dispatch was last updated", "format": "date-time" } }, @@ -12843,6 +13218,7 @@ "properties": { "assertionType": { "type": "string", + "description": "Assertion type evaluated", "enum": [ "status_code", "response_time", @@ -12889,24 +13265,29 @@ ] }, "passed": { - "type": "boolean" + "type": "boolean", + "description": "Whether the assertion passed" }, "severity": { "type": "string", + "description": "Assertion severity: FAIL or WARN", "enum": [ "fail", "warn" ] }, "message": { - "type": "string" + "type": "string", + "description": "Human-readable result description" }, "expected": { "type": "string", + "description": "Expected value", "nullable": true }, "actual": { "type": "string", + "description": "Actual value observed during the test", "nullable": true } } @@ -13158,26 +13539,32 @@ "properties": { "monitorId": { "type": "string", + "description": "Monitor to attach this maintenance window to; null for org-wide", "format": "uuid" }, "startsAt": { "type": "string", + "description": "Scheduled start of the maintenance window (ISO 8601)", "format": "date-time" }, "endsAt": { "type": "string", + "description": "Scheduled end of the maintenance window (ISO 8601)", "format": "date-time" }, "repeatRule": { "maxLength": 100, "minLength": 0, - "type": "string" + "type": "string", + "description": "iCal RRULE for recurring windows (max 100 chars); null for one-time" }, "reason": { - "type": "string" + "type": "string", + "description": "Human-readable reason for the maintenance" }, "suppressAlerts": { - "type": "boolean" + "type": "boolean", + "description": "Whether to suppress alerts during this window (default: true)" } } }, @@ -13191,30 +13578,36 @@ "email": { "minLength": 1, "type": "string", + "description": "Email address to invite", "format": "email" }, "roleOffered": { "type": "string", + "description": "Role to assign on acceptance", "enum": [ "OWNER", "ADMIN", "MEMBER" ] } - } + }, + "description": "Invite a new member to the organization by email" }, "InviteDto": { "type": "object", "properties": { "inviteId": { "type": "integer", + "description": "Unique invite identifier", "format": "int32" }, "email": { - "type": "string" + "type": "string", + "description": "Email address the invite was sent to" }, "roleOffered": { "type": "string", + "description": "Role that will be assigned to the invitee on acceptance", "enum": [ "OWNER", "ADMIN", @@ -13223,19 +13616,23 @@ }, "expiresAt": { "type": "string", + "description": "Timestamp when the invite expires", "format": "date-time" }, "consumedAt": { "type": "string", + "description": "Timestamp when the invite was accepted; null if not yet used", "format": "date-time", "nullable": true }, "revokedAt": { "type": "string", + "description": "Timestamp when the invite was revoked; null if active", "format": "date-time", "nullable": true } - } + }, + "description": "Organization invite sent to an email address" }, "SingleValueResponseInviteDto": { "type": "object", @@ -13356,10 +13753,12 @@ "type": "object", "properties": { "body": { - "type": "string" + "type": "string", + "description": "Update message or post-mortem notes" }, "newStatus": { "type": "string", + "description": "Updated incident status; null to keep current status", "enum": [ "WATCHING", "TRIGGERED", @@ -13368,7 +13767,8 @@ ] }, "notifySubscribers": { - "type": "boolean" + "type": "boolean", + "description": "Whether to notify subscribers of this update" } } }, @@ -13430,10 +13830,10 @@ }, "ttlMinutes": { "type": "integer", - "description": "Lock TTL in minutes. Defaults to 10. Max 60.", + "description": "Lock TTL in minutes (default: 30, max: 60)", "format": "int32", "nullable": true, - "example": 10 + "example": 30 } }, "description": "Request to acquire a deploy lock for the current workspace" @@ -13496,24 +13896,30 @@ "properties": { "id": { "type": "integer", + "description": "Unique API key identifier", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable name for this API key" }, "key": { - "type": "string" + "type": "string", + "description": "Full API key value in dh_live_* format; store this now" }, "createdAt": { "type": "string", + "description": "Timestamp when the key was created", "format": "date-time" }, "expiresAt": { "type": "string", + "description": "Timestamp when the key expires; null if no expiration", "format": "date-time", "nullable": true } - } + }, + "description": "Created API key with the full key value \u2014 store it now, it won't be shown again" }, "SingleValueResponseApiKeyCreateResponse": { "type": "object", @@ -13528,38 +13934,47 @@ "properties": { "id": { "type": "integer", + "description": "Unique API key identifier", "format": "int32" }, "name": { - "type": "string" + "type": "string", + "description": "Human-readable name for this API key" }, "key": { - "type": "string" + "type": "string", + "description": "Full API key value in dh_live_* format" }, "createdAt": { "type": "string", + "description": "Timestamp when the key was created", "format": "date-time" }, "updatedAt": { "type": "string", + "description": "Timestamp when the key was last updated", "format": "date-time" }, "lastUsedAt": { "type": "string", + "description": "Timestamp of the most recent API call; null if never used", "format": "date-time", "nullable": true }, "revokedAt": { "type": "string", + "description": "Timestamp when the key was revoked; null if active", "format": "date-time", "nullable": true }, "expiresAt": { "type": "string", + "description": "Timestamp when the key expires; null if no expiration", "format": "date-time", "nullable": true } - } + }, + "description": "API key for programmatic access to the DevHelm API" }, "SingleValueResponseApiKeyDto": { "type": "object", @@ -13667,7 +14082,8 @@ } ] } - } + }, + "description": "Alert channel configuration to test without saving" }, "ComponentUpdateRequest": { "required": [ @@ -13736,8 +14152,10 @@ "data": { "type": "object", "additionalProperties": { - "type": "string" - } + "type": "string", + "nullable": true + }, + "nullable": true } } }, @@ -13746,6 +14164,7 @@ "properties": { "data": { "type": "array", + "nullable": true, "items": { "$ref": "#/components/schemas/MonitorAssertionDto" } @@ -13757,10 +14176,12 @@ "properties": { "id": { "type": "string", + "description": "Unique monitor identifier", "format": "uuid" }, "type": { "type": "string", + "description": "Monitor protocol type", "enum": [ "HTTP", "DNS", @@ -13794,16 +14215,20 @@ }, "frequencySeconds": { "type": "integer", + "description": "Check frequency in seconds", "format": "int32" }, "regions": { "type": "array", + "description": "Probe regions to execute checks from", "items": { - "type": "string" + "type": "string", + "description": "Probe regions to execute checks from" } }, "organizationId": { "type": "integer", + "description": "Organization this monitor belongs to", "format": "int32" } } @@ -13830,6 +14255,7 @@ "properties": { "data": { "type": "array", + "nullable": true, "items": { "$ref": "#/components/schemas/BillingPlanDto" } @@ -13883,6 +14309,7 @@ "properties": { "action": { "type": "string", + "description": "Type of subscription action being previewed", "enum": [ "UPGRADE", "DOWNGRADE", @@ -13891,18 +14318,22 @@ }, "immediateAmount": { "type": "integer", + "description": "Amount due immediately (proration) in smallest currency unit", "format": "int32" }, "nextBillingAmount": { "type": "integer", + "description": "Amount that will be charged on the next billing cycle", "format": "int32" }, "nextBillingDate": { "type": "string", + "description": "Date of the next billing cycle; null if cancelling", "format": "date-time", "nullable": true } - } + }, + "description": "Preview of upcoming subscription charge after a plan change" }, "EntitlementDto": { "type": "object", @@ -14020,12 +14451,15 @@ "properties": { "id": { "type": "integer", + "description": "Numeric identifier", "format": "int32" }, "value": { - "type": "string" + "type": "string", + "description": "Display label or value" } - } + }, + "description": "Generic id/value pair for select options and autocomplete" }, "TableValueResultIdValuePair": { "type": "object", @@ -14049,13 +14483,16 @@ "properties": { "orgId": { "type": "integer", + "description": "Organization identifier", "format": "int32" }, "orgName": { - "type": "string" + "type": "string", + "description": "Organization name" }, "orgRole": { "type": "string", + "description": "Member role within this organization", "enum": [ "OWNER", "ADMIN", @@ -14064,6 +14501,7 @@ }, "status": { "type": "string", + "description": "Membership status", "enum": [ "INVITED", "ACTIVE", @@ -14073,7 +14511,8 @@ "DECLINED" ] } - } + }, + "description": "Membership summary for an organization the user belongs to" }, "TableValueResultMyOrgItemDto": { "type": "object", @@ -14331,13 +14770,15 @@ "type": "string", "description": "Human-readable description of when this event fires" } - } + }, + "description": "List of all available webhook event types" }, "WebhookEventCatalogResponse": { "type": "object", "properties": { "data": { "type": "array", + "description": "List of all available webhook event types", "items": { "$ref": "#/components/schemas/WebhookEventCatalogEntry" } @@ -14436,32 +14877,40 @@ "properties": { "id": { "type": "string", + "description": "Component identifier", "format": "uuid" }, "name": { - "type": "string" + "type": "string", + "description": "Component name" }, "status": { - "type": "string" + "type": "string", + "description": "Component status at the time of the maintenance update" } - } + }, + "description": "A component affected by a scheduled maintenance window" }, "MaintenanceUpdateDto": { "type": "object", "properties": { "id": { "type": "string", + "description": "Unique update identifier", "format": "uuid" }, "status": { - "type": "string" + "type": "string", + "description": "Status at the time of this update" }, "body": { "type": "string", + "description": "Update message from the vendor", "nullable": true }, "displayAt": { "type": "string", + "description": "Timestamp when this update was posted", "format": "date-time", "nullable": true } @@ -14473,53 +14922,65 @@ "properties": { "id": { "type": "string", + "description": "Unique maintenance record identifier", "format": "uuid" }, "externalId": { - "type": "string" + "type": "string", + "description": "Vendor-assigned maintenance identifier" }, "title": { - "type": "string" + "type": "string", + "description": "Maintenance title as reported by the vendor" }, "status": { - "type": "string" + "type": "string", + "description": "Current maintenance status (scheduled, in_progress, completed)" }, "impact": { "type": "string", + "description": "Reported impact level", "nullable": true }, "shortlink": { "type": "string", + "description": "Vendor-provided short URL to the maintenance page", "nullable": true }, "scheduledFor": { "type": "string", + "description": "Timestamp when the maintenance is scheduled to begin", "format": "date-time", "nullable": true }, "scheduledUntil": { "type": "string", + "description": "Timestamp when the maintenance is scheduled to end", "format": "date-time", "nullable": true }, "startedAt": { "type": "string", + "description": "Timestamp when the maintenance actually started", "format": "date-time", "nullable": true }, "completedAt": { "type": "string", + "description": "Timestamp when the maintenance was completed", "format": "date-time", "nullable": true }, "affectedComponents": { "type": "array", + "description": "Components affected by this maintenance", "items": { "$ref": "#/components/schemas/MaintenanceComponentRef" } }, "updates": { "type": "array", + "description": "Status updates posted during the maintenance lifecycle", "items": { "$ref": "#/components/schemas/MaintenanceUpdateDto" } @@ -14890,18 +15351,22 @@ "properties": { "date": { "type": "string", + "description": "Date of the daily bucket (ISO 8601)", "format": "date-time" }, "partialOutageSeconds": { "type": "integer", + "description": "Seconds of partial outage observed on this day", "format": "int32" }, "majorOutageSeconds": { "type": "integer", + "description": "Seconds of major outage observed on this day", "format": "int32" }, "uptimePercentage": { "type": "number", + "description": "Computed uptime percentage for the day", "format": "double" }, "eventsJson": { @@ -14910,7 +15375,8 @@ "nullable": true }, "source": { - "type": "string" + "type": "string", + "description": "Data source: vendor_reported or incident_derived" } }, "description": "Daily uptime data for a component" @@ -14937,39 +15403,48 @@ "properties": { "totalServices": { "type": "integer", + "description": "Total number of services in the catalog", "format": "int32" }, "operationalCount": { "type": "integer", + "description": "Number of services currently fully operational", "format": "int32" }, "degradedCount": { "type": "integer", + "description": "Number of services with degraded status", "format": "int32" }, "partialOutageCount": { "type": "integer", + "description": "Number of services with partial outage", "format": "int32" }, "majorOutageCount": { "type": "integer", + "description": "Number of services with major outage", "format": "int32" }, "maintenanceCount": { "type": "integer", + "description": "Number of services currently under maintenance", "format": "int32" }, "activeIncidentCount": { "type": "integer", + "description": "Total number of active incidents across all services", "format": "int64" }, "servicesWithIssues": { "type": "array", + "description": "Services that are not fully operational", "items": { "$ref": "#/components/schemas/ServiceCatalogDto" } } - } + }, + "description": "Global status summary across all subscribed vendor services" }, "SingleValueResponseGlobalStatusSummaryDto": { "type": "object", @@ -15043,34 +15518,43 @@ "properties": { "id": { "type": "integer", + "description": "Unique notification identifier", "format": "int64" }, "type": { - "type": "string" + "type": "string", + "description": "Notification category (e.g. incident, monitor, team)" }, "title": { - "type": "string" + "type": "string", + "description": "Short notification title" }, "body": { "type": "string", + "description": "Full notification body; null for title-only notifications", "nullable": true }, "resourceType": { "type": "string", + "description": "Type of the resource this notification is about", "nullable": true }, "resourceId": { "type": "string", + "description": "ID of the resource this notification is about", "nullable": true }, "read": { - "type": "boolean" + "type": "boolean", + "description": "Whether the notification has been read" }, "createdAt": { "type": "string", + "description": "Timestamp when the notification was created", "format": "date-time" } - } + }, + "description": "In-app notification for the current user" }, "TableValueResultNotificationDto": { "type": "object", @@ -15094,7 +15578,8 @@ "properties": { "data": { "type": "integer", - "format": "int64" + "format": "int64", + "nullable": true } } }, @@ -15154,14 +15639,17 @@ "properties": { "id": { "type": "string", + "description": "Unique version record identifier", "format": "uuid" }, "monitorId": { "type": "string", + "description": "Monitor this version belongs to", "format": "uuid" }, "version": { "type": "integer", + "description": "Monotonically increasing version number", "format": "int32" }, "snapshot": { @@ -15169,11 +15657,13 @@ }, "changedById": { "type": "integer", + "description": "User ID who made the change; null for automated changes", "format": "int32", "nullable": true }, "changedVia": { "type": "string", + "description": "Change source (DASHBOARD, CLI, API)", "enum": [ "API", "DASHBOARD", @@ -15183,13 +15673,16 @@ }, "changeSummary": { "type": "string", + "description": "Human-readable description of what changed", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when this version was recorded", "format": "date-time" } - } + }, + "description": "A point-in-time version snapshot of a monitor configuration" }, "TableValueResultMonitorVersionDto": { "type": "object", @@ -16177,7 +16670,8 @@ "incidents": { "$ref": "#/components/schemas/IncidentsSummaryDto" } - } + }, + "description": "Combined dashboard overview for monitors and incidents" }, "IncidentsSummaryDto": { "type": "object", @@ -16195,42 +16689,51 @@ "format": "double", "nullable": true } - } + }, + "description": "Incident summary counters" }, "MonitorsSummaryDto": { "type": "object", "properties": { "total": { "type": "integer", + "description": "Total number of monitors in the organization", "format": "int64" }, "up": { "type": "integer", + "description": "Number of monitors currently passing", "format": "int64" }, "down": { "type": "integer", + "description": "Number of monitors currently failing (DOWN severity)", "format": "int64" }, "degraded": { "type": "integer", + "description": "Number of monitors with degraded status", "format": "int64" }, "paused": { "type": "integer", + "description": "Number of disabled monitors", "format": "int64" }, "avgUptime24h": { "type": "number", + "description": "Average uptime percentage across all monitors over last 24h", "format": "double", "nullable": true }, "avgUptime30d": { "type": "number", + "description": "Average uptime percentage across all monitors over last 30 days", "format": "double", "nullable": true } - } + }, + "description": "Dashboard summary counters for monitors" }, "SingleValueResponseDashboardOverviewDto": { "type": "object", @@ -16244,13 +16747,16 @@ "type": "object", "properties": { "category": { - "type": "string" + "type": "string", + "description": "Category name (e.g. CI/CD, Cloud, Payments)" }, "serviceCount": { "type": "integer", + "description": "Number of services in this category", "format": "int64" } - } + }, + "description": "Service category with its count of catalog entries" }, "TableValueResultCategoryDto": { "type": "object", @@ -16417,42 +16923,52 @@ "properties": { "id": { "type": "integer", + "description": "Unique audit event identifier", "format": "int64" }, "actorId": { "type": "integer", + "description": "User ID who performed the action; null for system actions", "format": "int32", "nullable": true }, "actorEmail": { "type": "string", + "description": "Email of the actor; null for system actions", "nullable": true }, "action": { - "type": "string" + "type": "string", + "description": "Audit action type (e.g. monitor.created, api_key.revoked)" }, "resourceType": { "type": "string", + "description": "Type of resource affected (e.g. monitor, api_key)", "nullable": true }, "resourceId": { "type": "string", + "description": "ID of the affected resource", "nullable": true }, "resourceName": { "type": "string", + "description": "Human-readable name of the affected resource", "nullable": true }, "metadata": { "type": "object", "additionalProperties": { "type": "object", + "description": "Additional context about the action", "nullable": true }, + "description": "Additional context about the action", "nullable": true }, "createdAt": { "type": "string", + "description": "Timestamp when the action was performed", "format": "date-time" } } diff --git a/scripts/extract-descriptions.mjs b/scripts/extract-descriptions.mjs index 7b63448..2a03fee 100644 --- a/scripts/extract-descriptions.mjs +++ b/scripts/extract-descriptions.mjs @@ -16,7 +16,8 @@ const OUT_PATH = resolve(__dirname, '../src/lib/descriptions.generated.ts'); const spec = JSON.parse(readFileSync(SPEC_PATH, 'utf8')); const schemas = spec.components?.schemas ?? {}; -// Schemas we care about for CLI flag descriptions +// Schemas we care about for CLI flag descriptions. +// Keep in sync with MUST_HAVE in tests/spec/test_openapi_descriptions.py (monorepo). const TARGET_SCHEMAS = [ 'CreateMonitorRequest', 'UpdateMonitorRequest', 'CreateManualIncidentRequest', @@ -29,6 +30,7 @@ const TARGET_SCHEMAS = [ 'CreateWebhookEndpointRequest', 'UpdateWebhookEndpointRequest', 'CreateApiKeyRequest', 'UpdateApiKeyRequest', 'ResolveIncidentRequest', 'MonitorTestRequest', + 'AcquireDeployLockRequest', 'HttpMonitorConfig', 'TcpMonitorConfig', 'DnsMonitorConfig', 'IcmpMonitorConfig', 'HeartbeatMonitorConfig', 'McpServerMonitorConfig', 'SlackChannelConfig', 'DiscordChannelConfig', 'EmailChannelConfig', diff --git a/src/lib/api.generated.ts b/src/lib/api.generated.ts index 078e5c8..7743e99 100644 --- a/src/lib/api.generated.ts +++ b/src/lib/api.generated.ts @@ -2736,145 +2736,278 @@ export interface components { /** Format: int32 */ priceId?: number; }; + /** @description Associated billing plan; null when not requested */ BillingPlanDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Unique billing plan identifier + */ id?: number; + /** @description Paddle product identifier */ paddleId?: string; + /** @description Billing plan display name */ name?: string; + /** @description Plan description */ description?: string | null; + /** @description Available prices for this plan; null when not requested */ prices?: components["schemas"]["BillingPriceDto"][] | null; } | null; + /** @description Price details for this line item */ BillingPriceDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Unique billing price identifier + */ id?: number; + /** @description Paddle price identifier */ paddleId?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Price amount in smallest currency unit (e.g. cents) + */ amount?: number; - /** @enum {string} */ + /** + * @description Billing interval (MONTH or YEAR) + * @enum {string} + */ interval?: "DAY" | "WEEK" | "MONTH" | "YEAR"; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of intervals between billing cycles + */ intervalCount?: number; + /** @description Price description */ description?: string | null; billingPlan?: components["schemas"]["BillingPlanDto"]; }; + /** @description Line items included in this subscription */ ItemDto: { billingPrice?: components["schemas"]["BillingPriceDto"]; - /** Format: int32 */ + /** + * Format: int32 + * @description Quantity of this price + */ quantity?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Line item total in smallest currency unit + */ amount?: number; }; SingleValueResponseSubscriptionDto: { data?: components["schemas"]["SubscriptionDto"]; }; + /** @description Current billing subscription details */ SubscriptionDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Internal subscription identifier + */ id?: number; + /** @description Paddle subscription identifier */ paddleId?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the subscription was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the subscription was last updated + */ updatedAt?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this subscription belongs to + */ organizationId?: number; - /** @enum {string} */ + /** + * @description Current subscription status + * @enum {string} + */ status?: "ACTIVE" | "CANCELED" | "PAST_DUE" | "PAUSED" | "TRIALING"; - /** Format: date-time */ + /** + * Format: date-time + * @description Next billing date; null when cancelled or expired + */ nextBilledAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Scheduled cancellation date; null if no cancellation pending + */ willCancelAt?: string | null; + /** @description Line items included in this subscription */ items?: components["schemas"]["ItemDto"][]; }; UpdateOrgDetailsRequest: { + /** @description New organization name (max 200 chars) */ name: string; - /** Format: email */ + /** + * Format: email + * @description New billing and contact email address + */ email: string; + /** @description Team size range (e.g. 1-10, 11-50) */ size?: string; + /** @description Industry vertical (e.g. SaaS, Fintech) */ industry?: string; + /** @description Organization website URL (max 255 chars) */ websiteUrl?: string; }; + /** @description Organization account details */ OrganizationDto: { - /** Format: int32 */ + /** + * 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; }; SingleValueResponseOrganizationDto: { data?: components["schemas"]["OrganizationDto"]; }; + /** @description Advance the user's onboarding stage */ UpdateOnboardingStageRequest: { - /** @enum {string} */ + /** + * @description New onboarding stage + * @enum {string} + */ stage: "WELCOME" | "FIRST_MONITOR" | "SETUP_COMPLETE" | "COMPLETED"; }; SingleValueResponseUserDto: { data?: components["schemas"]["UserDto"]; }; + /** @description User account details */ UserDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Unique user identifier + */ id?: number; + /** @description User email address */ email?: string; + /** @description Whether the email address has been verified */ emailVerified?: boolean; + /** @description Display name; null if not set */ name?: string | null; - /** @enum {string} */ + /** + * @description Platform role: USER or SUPERADMIN + * @enum {string} + */ userRole?: "SUPERADMIN" | "ADMIN" | "USER"; - /** @enum {string|null} */ + /** + * @description Current onboarding progress stage; null when completed + * @enum {string|null} + */ onboardingStage?: "WELCOME" | "FIRST_MONITOR" | "SETUP_COMPLETE" | "COMPLETED" | null; + /** @description Profile image URL; null if not set */ imageUrl?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the account was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the account was last updated + */ updatedAt?: string; }; UpdateProfileRequest: { + /** @description New display name (max 200 chars) */ name?: string; }; + /** @description Replace notification preferences for the current user */ UpdateNotificationPreferencesRequest: { + /** @description Map of category keys to enabled/disabled flags */ preferences: { [key: string]: boolean; }; }; + /** @description User notification preferences keyed by notification category */ NotificationPreferencesDto: { + /** @description Map of category keys to enabled/disabled flags */ preferences?: { [key: string]: boolean; }; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when preferences were last updated + */ updatedAt?: string; }; SingleValueResponseNotificationPreferencesDto: { data?: components["schemas"]["NotificationPreferencesDto"]; }; + /** @description Update workspace details */ UpdateWorkspaceRequest: { + /** @description New workspace name */ name: string; }; SingleValueResponseWorkspaceDto: { data?: components["schemas"]["WorkspaceDto"]; }; + /** @description Workspace within an organization */ WorkspaceDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Unique workspace identifier + */ id?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the workspace was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the workspace was last updated + */ updatedAt?: string; + /** @description Workspace name */ name?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this workspace belongs to + */ orgId?: number; }; UpdateUserRequest: { + /** @description New display name (max 200 chars) */ name?: string; - /** Format: email */ + /** + * Format: email + * @description New email address + */ email?: string; - /** @enum {string} */ + /** + * @description New platform role + * @enum {string} + */ userRole?: "SUPERADMIN" | "ADMIN" | "USER"; - /** @enum {string} */ + /** + * @description New onboarding stage + * @enum {string} + */ onboardingStage?: "WELCOME" | "FIRST_MONITOR" | "SETUP_COMPLETE" | "COMPLETED"; + /** @description New profile image URL (max 500 chars) */ imageUrl?: string; }; + /** @description Update an organization member's role */ ChangeRoleRequest: { - /** @enum {string} */ + /** + * @description New role to assign + * @enum {string} + */ orgRole: "OWNER" | "ADMIN" | "MEMBER"; }; UpdateWebhookEndpointRequest: { @@ -2890,21 +3023,42 @@ export interface components { SingleValueResponseWebhookEndpointDto: { data?: components["schemas"]["WebhookEndpointDto"]; }; + /** @description Webhook endpoint that receives event delivery payloads */ WebhookEndpointDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique webhook endpoint identifier + */ id?: string; + /** @description HTTPS endpoint URL that receives event payloads */ url?: string; + /** @description Human-readable description of this endpoint */ description?: string | null; + /** @description Event types this endpoint is subscribed to */ subscribedEvents?: string[]; + /** @description Whether delivery is enabled for this endpoint */ enabled?: boolean; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of consecutive delivery failures + */ consecutiveFailures?: number; + /** @description Reason the endpoint was auto-disabled */ disabledReason?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the endpoint was auto-disabled + */ disabledAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the endpoint was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the endpoint was last updated + */ updatedAt?: string; }; /** @description Request body for updating a tag; null fields are left unchanged */ @@ -2917,38 +3071,74 @@ export interface components { SingleValueResponseTagDto: { data?: components["schemas"]["TagDto"]; }; + /** @description Tag for organizing and filtering monitors */ TagDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique tag identifier + */ id?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this tag belongs to + */ organizationId?: number; + /** @description Tag name, unique within the org */ name?: string; + /** @description Hex color code for display (e.g. #6B7280) */ color?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the tag was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the tag was last updated + */ updatedAt?: string; }; UpdateSecretRequest: { /** @description New secret value, stored encrypted (max 32KB) */ value: string; }; + /** @description Monitors that reference this secret; null on create/update responses */ MonitorReference: { - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor identifier + */ id?: string; + /** @description Monitor name */ name?: string; }; + /** @description Secret with change-detection hash; plaintext value is never returned */ SecretDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique secret identifier + */ id?: string; + /** @description Secret key name, unique within the workspace */ key?: string; - /** Format: int32 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the secret was created + */ createdAt?: string; - /** Format: date-time */ + /** + * 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; }; SingleValueResponseSecretDto: { @@ -2956,10 +3146,17 @@ export interface components { }; /** @description Default retry strategy for member monitors; null clears */ RetryStrategy: { + /** @description Retry strategy kind, e.g. fixed interval between attempts */ type: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum number of retries after a failed check + */ maxRetries?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Delay between retry attempts in seconds + */ interval?: number; }; /** @description Request body for updating a resource group */ @@ -3010,12 +3207,21 @@ export interface components { }; /** @description Resource group with health summary and optional member details */ ResourceGroupDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique resource group identifier + */ id?: string; - /** Format: int32 */ + /** + * 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 @@ -3059,9 +3265,15 @@ export interface components { health?: components["schemas"]["ResourceGroupHealthDto"]; /** @description Member list with individual statuses; populated on detail GET only */ members?: components["schemas"]["ResourceGroupMemberDto"][] | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the group was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the group was last updated + */ updatedAt?: string; }; /** @description Aggregated health summary for a resource group */ @@ -3099,9 +3311,15 @@ export interface components { }; /** @description A single member of a resource group with its computed health status */ ResourceGroupMemberDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique group member record identifier + */ id?: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Resource group this member belongs to + */ groupId?: string; /** @description Type of member: 'monitor' or 'service' */ memberType?: string; @@ -3131,7 +3349,10 @@ export interface components { 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 */ + /** + * Format: date-time + * @description Timestamp when the member was added to the group + */ createdAt?: string; /** * Format: double @@ -3219,9 +3440,15 @@ export interface components { }; /** @description Org-level notification policy with match rules and escalation chain */ NotificationPolicyDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique notification policy identifier + */ id?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this policy belongs to + */ organizationId?: number; /** @description Human-readable name for this policy */ name?: string; @@ -3235,9 +3462,15 @@ export interface components { * @description Evaluation order; higher value = evaluated first */ priority?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the policy was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the policy was last updated + */ updatedAt?: string; }; SingleValueResponseNotificationPolicyDto: { @@ -3245,26 +3478,47 @@ export interface components { }; /** @description Multi-region confirmation settings */ ConfirmationPolicy: { - /** @enum {string} */ + /** + * @description How incident confirmation is coordinated across regions + * @enum {string} + */ type: "multi_region"; - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum failing regions required to confirm an incident + */ minRegionsFailing?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum seconds to wait for enough regions to fail after first trigger + */ maxWaitSeconds?: number; }; /** @description Incident detection, confirmation, and recovery policy for a monitor */ IncidentPolicyDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique incident policy identifier + */ id?: string; - /** Format: uuid */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the policy was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the policy was last updated + */ updatedAt?: string; /** * Format: int32 @@ -3279,28 +3533,58 @@ export interface components { }; /** @description Auto-recovery settings */ RecoveryPolicy: { - /** Format: int32 */ + /** + * Format: int32 + * @description Consecutive passing checks required to auto-resolve the incident + */ consecutiveSuccesses?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum regions that must be passing before recovery can complete + */ minRegionsPassing?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Minutes after resolve before a new incident may open on the same monitor + */ cooldownMinutes?: number; }; /** @description Array of trigger rules defining when an incident should be raised */ TriggerRule: { - /** @enum {string} */ + /** + * @description Condition that opens or escalates an incident from check results + * @enum {string} + */ type: "consecutive_failures" | "failures_in_window" | "response_time"; - /** Format: int32 */ + /** + * Format: int32 + * @description Failure count for consecutive or windowed failure rules + */ count?: number | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Window length in minutes for failures-in-window rules + */ windowMinutes?: number | null; - /** @enum {string|null} */ + /** + * @description Whether the rule applies per region or across regions + * @enum {string|null} + */ scope: "per_region" | "any_region" | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Response time threshold in milliseconds for response-time rules + */ thresholdMs?: number | null; - /** @enum {string} */ + /** + * @description Incident severity when this rule fires + * @enum {string} + */ severity: "down" | "degraded"; - /** @enum {string|null} */ + /** + * @description How response times are aggregated for response-time rules + * @enum {string|null} + */ aggregationType?: "all_exceed" | "average" | "p95" | "max" | null; }; /** @description Request body for updating an incident policy */ @@ -3316,29 +3600,44 @@ export interface components { ApiKeyAuthConfig: { type: "ApiKeyAuthConfig"; } & (Omit & { + /** @description HTTP header name that carries the API key */ headerName: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Vault secret ID for the API key value + */ vaultSecretId?: string | null; }); BasicAuthConfig: { type: "BasicAuthConfig"; } & (Omit & { - /** Format: uuid */ + /** + * Format: uuid + * @description Vault secret ID holding Basic auth username and password + */ vaultSecretId?: string | null; }); BearerAuthConfig: { type: "BearerAuthConfig"; } & (Omit & { - /** Format: uuid */ + /** + * Format: uuid + * @description Vault secret ID holding the bearer token value + */ vaultSecretId?: string | null; }); HeaderAuthConfig: { type: "HeaderAuthConfig"; } & (Omit & { + /** @description Custom HTTP header name for the secret value */ headerName: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Vault secret ID for the header value + */ vaultSecretId?: string | null; }); + /** @description New authentication configuration (full replacement) */ MonitorAuthConfig: { type: string; }; @@ -3357,48 +3656,64 @@ export interface components { SingleValueResponseMonitorAuthDto: { data?: components["schemas"]["MonitorAuthDto"]; }; + /** @description New assertion configuration (full replacement) */ AssertionConfig: { type: string; }; BodyContainsAssertion: { type: "BodyContainsAssertion"; } & (Omit & { + /** @description Substring that must appear in the response body */ substring: string; }); DnsExpectedCnameAssertion: { type: "DnsExpectedCnameAssertion"; } & (Omit & { + /** @description Expected CNAME target the resolution must include */ value: string; }); DnsExpectedIpsAssertion: { type: "DnsExpectedIpsAssertion"; } & (Omit & { + /** @description Allowed IP addresses; at least one resolved address must match */ ips: string[]; }); DnsMaxAnswersAssertion: { type: "DnsMaxAnswersAssertion"; } & (Omit & { + /** @description DNS record type whose answer count is checked */ recordType: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum number of answers allowed for that record type + */ max?: number; }); DnsMinAnswersAssertion: { type: "DnsMinAnswersAssertion"; } & (Omit & { + /** @description DNS record type whose answer count is checked */ recordType: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum number of answers required for that record type + */ min?: number; }); DnsRecordContainsAssertion: { type: "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; }); DnsRecordEqualsAssertion: { type: "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; }); DnsResolvesAssertion: { @@ -3407,56 +3722,82 @@ export interface components { DnsResponseTimeAssertion: { type: "DnsResponseTimeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum allowed DNS resolution time in milliseconds + */ maxMs?: number; }); DnsResponseTimeWarnAssertion: { type: "DnsResponseTimeWarnAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description DNS resolution time in milliseconds that triggers a warning only + */ warnMs?: number; }); DnsTtlHighAssertion: { type: "DnsTtlHighAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum TTL in seconds before a high-TTL warning is raised + */ maxTtl?: number; }); DnsTtlLowAssertion: { type: "DnsTtlLowAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum acceptable TTL in seconds before a warning is raised + */ minTtl?: number; }); DnsTxtContainsAssertion: { type: "DnsTxtContainsAssertion"; } & (Omit & { + /** @description Substring that must appear in at least one TXT record */ substring: string; }); HeaderValueAssertion: { type: "HeaderValueAssertion"; } & (Omit & { + /** @description HTTP header name to assert on */ headerName: string; + /** @description Expected value to compare against */ expected: string; - /** @enum {string} */ + /** + * @description Comparison operator (equals, contains, less_than, greater_than, etc.) + * @enum {string} + */ operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; }); HeartbeatIntervalDriftAssertion: { type: "HeartbeatIntervalDriftAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Max percent drift from expected ping interval before warning (non-fatal) + */ maxDeviationPercent: number; }); HeartbeatMaxIntervalAssertion: { type: "HeartbeatMaxIntervalAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum allowed gap in seconds between consecutive heartbeat pings + */ maxSeconds: number; }); HeartbeatPayloadContainsAssertion: { type: "HeartbeatPayloadContainsAssertion"; } & (Omit & { + /** @description JSONPath expression into the heartbeat ping JSON payload */ path: string; + /** @description Expected value to compare against at that path */ value: string; }); HeartbeatReceivedAssertion: { @@ -3465,7 +3806,10 @@ export interface components { IcmpPacketLossAssertion: { type: "IcmpPacketLossAssertion"; } & (Omit & { - /** Format: double */ + /** + * Format: double + * @description Maximum allowed packet loss percentage before the check fails (0–100) + */ maxPercent?: number; }); IcmpReachableAssertion: { @@ -3474,21 +3818,32 @@ export interface components { IcmpResponseTimeAssertion: { type: "IcmpResponseTimeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum average ICMP round-trip time in milliseconds + */ maxMs?: number; }); IcmpResponseTimeWarnAssertion: { type: "IcmpResponseTimeWarnAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description ICMP round-trip time in milliseconds that triggers a warning only + */ warnMs?: number; }); JsonPathAssertion: { type: "JsonPathAssertion"; } & (Omit & { + /** @description JSONPath expression to extract a value from the response body */ path: string; + /** @description Expected value to compare against */ expected: string; - /** @enum {string} */ + /** + * @description Comparison operator (equals, contains, less_than, greater_than, etc.) + * @enum {string} + */ operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; }); McpConnectsAssertion: { @@ -3497,89 +3852,128 @@ export interface components { McpHasCapabilityAssertion: { type: "McpHasCapabilityAssertion"; } & (Omit & { + /** @description Capability name the server must advertise, e.g. tools or resources */ capability: string; }); McpMinToolsAssertion: { type: "McpMinToolsAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum number of tools the server must expose + */ min?: number; }); McpProtocolVersionAssertion: { type: "McpProtocolVersionAssertion"; } & (Omit & { + /** @description Expected MCP protocol version string from the server handshake */ version: string; }); McpResponseTimeAssertion: { type: "McpResponseTimeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum allowed MCP check duration in milliseconds + */ maxMs?: number; }); McpResponseTimeWarnAssertion: { type: "McpResponseTimeWarnAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description MCP check duration in milliseconds that triggers a warning only + */ warnMs?: number; }); McpToolAvailableAssertion: { type: "McpToolAvailableAssertion"; } & (Omit & { + /** @description MCP tool name that must appear in the server's tool list */ toolName: string; }); McpToolCountChangedAssertion: { type: "McpToolCountChangedAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Expected tool count; warns when the live count differs + */ expectedCount?: number; }); RedirectCountAssertion: { type: "RedirectCountAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum number of HTTP redirects allowed before the check fails + */ maxCount?: number; }); RedirectTargetAssertion: { type: "RedirectTargetAssertion"; } & (Omit & { + /** @description Expected final URL after following redirects */ expected: string; - /** @enum {string} */ + /** + * @description Comparison operator (equals, contains, less_than, greater_than, etc.) + * @enum {string} + */ operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; }); RegexBodyAssertion: { type: "RegexBodyAssertion"; } & (Omit & { + /** @description Regular expression the response body must match */ pattern: string; }); ResponseSizeAssertion: { type: "ResponseSizeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum response body size in bytes before the check fails + */ maxBytes?: number; }); ResponseTimeAssertion: { type: "ResponseTimeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum allowed response time in milliseconds before the check fails + */ thresholdMs?: number; }); ResponseTimeWarnAssertion: { type: "ResponseTimeWarnAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description HTTP response time in milliseconds that triggers a warning only + */ warnMs?: number; }); SslExpiryAssertion: { type: "SslExpiryAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Minimum days before TLS certificate expiry; fails or warns below this threshold + */ minDaysRemaining?: number; }); StatusCodeAssertion: { type: "StatusCodeAssertion"; } & (Omit & { + /** @description Expected status code, range pattern, or wildcard such as 2xx */ expected: string; - /** @enum {string} */ + /** + * @description Comparison operator (equals, contains, less_than, greater_than, etc.) + * @enum {string} + */ operator: "equals" | "contains" | "less_than" | "greater_than" | "matches" | "range"; }); TcpConnectsAssertion: { @@ -3588,18 +3982,27 @@ export interface components { TcpResponseTimeAssertion: { type: "TcpResponseTimeAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description Maximum TCP connect time in milliseconds before the check fails + */ maxMs?: number; }); TcpResponseTimeWarnAssertion: { type: "TcpResponseTimeWarnAssertion"; } & (Omit & { - /** Format: int32 */ + /** + * Format: int32 + * @description TCP connect time in milliseconds that triggers a warning only + */ warnMs?: number; }); UpdateAssertionRequest: { 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} */ + /** + * @description New outcome severity: FAIL or WARN + * @enum {string} + */ severity?: "fail" | "warn"; }; MonitorAssertionDto: { @@ -3616,11 +4019,13 @@ export interface components { SingleValueResponseMonitorAssertionDto: { data?: components["schemas"]["MonitorAssertionDto"]; }; + /** @description Replace the alert channels linked to a monitor */ SetAlertChannelsRequest: { + /** @description IDs of alert channels to link (replaces current list) */ channelIds: string[]; }; SingleValueResponseListUUID: { - data?: string[]; + data?: (string | null)[] | null; }; /** @description Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both. */ AddMonitorTagsRequest: { @@ -3632,7 +4037,10 @@ export interface components { /** @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"]; - /** @enum {string} */ + /** + * @description Outcome severity: FAIL (fails the check) or WARN (warns without failing) + * @enum {string} + */ severity?: "fail" | "warn"; }; DnsMonitorConfig: components["schemas"]["MonitorConfig"] & { @@ -3766,72 +4174,137 @@ export interface components { alertChannelIds?: (string | null)[] | null; tags?: components["schemas"]["AddMonitorTagsRequest"]; }; + /** @description Full monitor representation */ MonitorDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique monitor identifier + */ id?: string; - /** Format: int32 */ + /** + * 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 */ + /** + * 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[]; - /** @enum {string} */ + /** + * @description Management source: DASHBOARD or CLI + * @enum {string} + */ managedBy?: "DASHBOARD" | "CLI"; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the monitor was created + */ createdAt?: string; - /** Format: date-time */ + /** + * 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"]; auth?: components["schemas"]["ApiKeyAuthConfig"] | components["schemas"]["BasicAuthConfig"] | components["schemas"]["BearerAuthConfig"] | components["schemas"]["HeaderAuthConfig"]; incidentPolicy?: components["schemas"]["IncidentPolicyDto"]; + /** @description Alert channel IDs linked to this monitor; populated on single-monitor responses */ alertChannelIds?: (string | null)[] | null; }; SingleValueResponseMonitorDto: { data?: components["schemas"]["MonitorDto"]; }; + /** @description Environment associated with this monitor; null when unassigned */ Summary: { /** Format: uuid */ id?: string; name?: string; slug?: string; }; + /** @description Update an organization member's status */ ChangeStatusRequest: { - /** @enum {string} */ + /** + * @description New membership status (ACTIVE or SUSPENDED) + * @enum {string} + */ status: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; }; UpdateMaintenanceWindowRequest: { - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor to attach this maintenance window to; null preserves current + */ monitorId?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Updated start time (ISO 8601) + */ startsAt: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Updated end time (ISO 8601) + */ endsAt: string; + /** @description Updated iCal RRULE; null clears the repeat rule */ repeatRule?: string; + /** @description Updated reason; null clears the existing reason */ reason?: string; + /** @description Whether to suppress alerts; null preserves current */ suppressAlerts?: boolean; }; + /** @description Scheduled maintenance window for a monitor */ MaintenanceWindowDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique maintenance window identifier + */ id?: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor this window applies to; null for org-wide windows + */ monitorId?: string | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this maintenance window belongs to + */ organizationId?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description Scheduled start of the maintenance window + */ startsAt?: string; - /** Format: date-time */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the window was created + */ createdAt?: string; }; SingleValueResponseMaintenanceWindowDto: { @@ -3847,22 +4320,42 @@ export interface components { /** @description Whether this is the default environment; null preserves current */ isDefault?: boolean | null; }; + /** @description Environment with variable substitutions for monitor configs */ EnvironmentDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique environment identifier + */ id?: string; - /** Format: int32 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the environment was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the environment was last updated + */ updatedAt?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of monitors using this environment + */ monitorCount?: number; + /** @description Whether this is the default environment for new monitors */ isDefault?: boolean; }; SingleValueResponseEnvironmentDto: { @@ -3933,22 +4426,42 @@ export interface components { [key: string]: string | null; } | null; }); + /** @description Alert channel with non-sensitive configuration metadata */ AlertChannelDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique alert channel identifier + */ id: string; + /** @description Human-readable channel name */ name: string; - /** @enum {string} */ + /** + * @description Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL) + * @enum {string} + */ channelType: "email" | "webhook" | "slack" | "pagerduty" | "opsgenie" | "teams" | "discord"; + /** @description Non-sensitive display metadata; null for older channels */ displayConfig?: { [key: string]: Record | null; } | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the channel was created + */ createdAt: string; - /** Format: date-time */ + /** + * 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 */ + /** + * 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; }; SingleValueResponseAlertChannelDto: { @@ -3972,54 +4485,121 @@ export interface components { action: string; statusText?: string | null; }; + /** @description Incident triggered by a monitor check failure or manual creation */ IncidentDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique incident identifier + */ id?: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor that triggered the incident; null for service or manual incidents + */ monitorId?: string | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this incident belongs to + */ organizationId?: number; - /** @enum {string} */ + /** + * @description Incident origin: MONITOR, SERVICE, or MANUAL + * @enum {string} + */ source?: "AUTOMATIC" | "MANUAL" | "MONITORS" | "STATUS_DATA" | "RESOURCE_GROUP"; - /** @enum {string} */ + /** + * @description Current lifecycle status (OPEN, RESOLVED, etc.) + * @enum {string} + */ status?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED"; - /** @enum {string} */ + /** + * @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 */ + /** + * Format: int32 + * @description Number of times this incident has been reopened + */ reopenCount?: number; - /** Format: int32 */ + /** + * 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 */ + /** + * Format: uuid + * @description Linked vendor service incident ID; null for monitor incidents + */ serviceIncidentId?: string | null; - /** Format: uuid */ + /** + * 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)[] | null; + /** @description Short URL linking to the incident details */ shortlink?: string | null; - /** @enum {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 */ + /** + * Format: date-time + * @description Timestamp when the incident was detected or created + */ startedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the incident was confirmed (multi-region confirmation) + */ confirmedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the incident was resolved + */ resolvedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Cooldown window end; new incidents suppressed until this time + */ cooldownUntil?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the incident record was created + */ createdAt?: string; - /** Format: date-time */ + /** + * 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 */ + /** + * 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; }; TableValueResultIncidentDto: { @@ -4029,7 +4609,7 @@ export interface components { }; SingleValueResponseInteger: { /** Format: int32 */ - data?: number; + data?: number | null; }; CreateAutoIncidentRequest: { /** Format: uuid */ @@ -4054,126 +4634,245 @@ export interface components { errorMessage?: string | null; }; AdapterHealthDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Service this health record belongs to + */ serviceId?: string; + /** @description URL-safe service identifier */ serviceSlug?: string; + /** @description Service name */ serviceName?: string; + /** @description Data source adapter type */ adapterType?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp of the last successful poll + */ lastSuccessAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp of the last failed poll + */ lastFailureAt?: string | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of consecutive poll failures + */ consecutiveFailures?: number; + /** @description Error message from the most recent failure */ lastErrorMessage?: string | null; + /** @description Whether the adapter is disabled due to repeated failures */ disabledByHealth?: boolean; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when this health record was last updated + */ updatedAt?: string; }; SingleValueResponseAdapterHealthDto: { data?: components["schemas"]["AdapterHealthDto"]; }; + /** @description Create a new organization */ CreateOrgRequest: { + /** @description Organization name */ name: string; - /** Format: email */ + /** + * Format: email + * @description Billing and contact email address + */ email?: string | null; }; SingleValueResponseTransactionDto: { data?: components["schemas"]["TransactionDto"]; }; + /** @description A billing transaction from Paddle */ TransactionDto: { + /** @description Paddle transaction identifier */ id?: string; + /** @description Transaction status (e.g. completed, pending) */ status?: string | null; + /** @description ISO 4217 currency code */ currencyCode?: string | null; + /** @description Invoice number; null if not invoiced */ invoiceNumber?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the transaction was billed + */ billedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the transaction was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the transaction was last updated + */ updatedAt?: string; + /** @description Total amount as a decimal string (including tax) */ total?: string | null; + /** @description Subtotal before tax as a decimal string */ subtotal?: string | null; + /** @description Tax amount as a decimal string */ tax?: string | null; }; + /** @description Minimal request for creating an HTTP monitor quickly */ QuickMonitorRequest: { + /** @description Target URL to monitor */ url: string; + /** @description Human-readable monitor name; defaults to the hostname if omitted */ name?: string | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Check frequency in seconds (30–86400); defaults to 60 + */ frequencySeconds?: number | null; }; OnboardingSetupRequest: { + /** @description Organization or team name (max 200 chars) */ name: string; + /** @description User's role or job title */ role?: string | null; + /** @description Team size range (e.g. 1-10, 11-50) */ teamSize?: string | null; }; + /** @description URL to analyze for monitor setup suggestions */ AnalyzeUrlRequest: { + /** @description Target URL to analyze (must be a valid HTTP/HTTPS URL) */ url: string; }; + /** @description Analysis of a URL with monitor setup suggestions */ AnalyzeUrlResponse: { + /** @description Whether the URL responded during analysis */ reachable?: boolean; - /** Format: int64 */ + /** + * Format: int64 + * @description Response time observed during analysis in milliseconds + */ responseTimeMs?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description HTTP status code from the analysis request + */ statusCode?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description TLS certificate expiry date; null for non-HTTPS or unavailable + */ tlsExpiry?: string | null; - /** Format: int32 */ + /** + * Format: int32 + * @description Days until TLS certificate expires; null if not applicable + */ tlsDaysRemaining?: number | null; + /** @description Response Content-Type header value */ contentType?: string | null; + /** @description Suggested monitor name derived from the URL hostname */ suggestedName?: string; + /** @description Recommended assertions based on the URL response */ suggestedAssertions?: components["schemas"]["SuggestedAssertion"][]; - /** Format: int32 */ + /** + * Format: int32 + * @description Suggested check frequency in seconds based on the URL + */ suggestedFrequencySeconds?: number; }; SingleValueResponseAnalyzeUrlResponse: { data?: components["schemas"]["AnalyzeUrlResponse"]; }; + /** @description Recommended assertions based on the URL response */ SuggestedAssertion: { + /** @description Assertion type (e.g. status_code, response_time) */ type?: string; + /** @description Comparison operator (e.g. equals, less_than) */ operator?: string; + /** @description Expected value to compare against */ value?: string; }; + /** @description Accept an organization invite using the invite token */ AcceptInviteRequest: { + /** @description Invite token from the invitation email */ token: string; }; + /** @description Result of accepting an organization invite */ AcceptInviteDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Organization the user joined + */ orgId?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description User who accepted the invite + */ userId?: number; - /** @enum {string} */ + /** + * @description Role assigned to the new member + * @enum {string} + */ orgRole?: "OWNER" | "ADMIN" | "MEMBER"; - /** @enum {string} */ + /** + * @description Initial membership status after joining + * @enum {string} + */ status?: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; }; SingleValueResponseAcceptInviteDto: { data?: components["schemas"]["AcceptInviteDto"]; }; RegisterUserRequest: { + /** @description User nickname from the identity provider */ nickname?: string | null; + /** @description User display name from the identity provider */ name?: string | null; + /** @description Profile picture URL from the identity provider */ picture?: string | null; }; + /** @description Create a new workspace within the organization */ CreateWorkspaceRequest: { + /** @description Workspace name */ name: string; }; + /** @description Add an existing user as a member of the organization */ AddMemberRequest: { - /** Format: int32 */ + /** + * Format: int32 + * @description ID of the user to add + */ userId: number; - /** @enum {string} */ + /** + * @description Role to assign to the new member + * @enum {string} + */ orgRole: "OWNER" | "ADMIN" | "MEMBER"; }; + /** @description Organization member with role and status */ MemberDto: { - /** Format: int32 */ + /** + * 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; - /** @enum {string} */ + /** + * @description Member role within this organization (OWNER, ADMIN, MEMBER) + * @enum {string} + */ orgRole?: "OWNER" | "ADMIN" | "MEMBER"; - /** @enum {string} */ + /** + * @description Membership status (ACTIVE, PENDING, SUSPENDED) + * @enum {string} + */ status?: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the member was added to the organization + */ createdAt?: string; }; SingleValueResponseMemberDto: { @@ -4187,7 +4886,9 @@ export interface components { /** @description Event types to deliver, e.g. monitor.created, incident.resolved */ subscribedEvents: string[]; }; + /** @description Event type to use for a test webhook delivery */ TestWebhookEndpointRequest: { + /** @description Event type to simulate (e.g. monitor.created); null uses a default */ eventType?: string | null; }; SingleValueResponseWebhookTestResult: { @@ -4202,18 +4903,34 @@ export interface components { durationMs?: number | null; }; SingleValueResponseString: { - data?: string; + data?: string | null; }; + /** @description Result of a data encryption key rotation operation */ DekRotationResultDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description DEK version before rotation + */ previousDekVersion?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description DEK version after rotation + */ newDekVersion?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of secrets re-encrypted with the new DEK + */ secretsReEncrypted?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of alert channels re-encrypted with the new DEK + */ channelsReEncrypted?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the rotation was performed + */ rotatedAt?: string; }; SingleValueResponseDekRotationResultDto: { @@ -4536,11 +5253,20 @@ export interface components { }; /** @description Dispatch state for a single (incident, notification policy) pair, with delivery history */ NotificationDispatchDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique dispatch record identifier + */ id?: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Incident this dispatch is for + */ incidentId?: string; - /** Format: uuid */ + /** + * 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; @@ -4581,9 +5307,15 @@ export interface components { lastNotifiedAt?: string | null; /** @description Delivery records for all channels associated with this dispatch */ deliveries?: components["schemas"]["AlertDeliveryDto"][]; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the dispatch was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the dispatch was last updated + */ updatedAt?: string; }; SingleValueResponseNotificationDispatchDto: { @@ -4629,13 +5361,23 @@ export interface components { config: components["schemas"]["ApiKeyAuthConfig"] | components["schemas"]["BasicAuthConfig"] | components["schemas"]["BearerAuthConfig"] | components["schemas"]["HeaderAuthConfig"]; }; AssertionTestResultDto: { - /** @enum {string} */ + /** + * @description Assertion type evaluated + * @enum {string} + */ assertionType?: "status_code" | "response_time" | "body_contains" | "json_path" | "header" | "regex" | "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; - /** @enum {string} */ + /** + * @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; }; MonitorTestResultDto: { @@ -4710,33 +5452,69 @@ export interface components { data?: components["schemas"]["BulkMonitorActionResult"]; }; CreateMaintenanceWindowRequest: { - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor to attach this maintenance window to; null for org-wide + */ monitorId?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Scheduled start of the maintenance window (ISO 8601) + */ startsAt: string; - /** Format: date-time */ + /** + * 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; + /** @description Human-readable reason for the maintenance */ reason?: string; + /** @description Whether to suppress alerts during this window (default: true) */ suppressAlerts?: boolean; }; + /** @description Invite a new member to the organization by email */ CreateInviteRequest: { - /** Format: email */ + /** + * Format: email + * @description Email address to invite + */ email: string; - /** @enum {string} */ + /** + * @description Role to assign on acceptance + * @enum {string} + */ roleOffered: "OWNER" | "ADMIN" | "MEMBER"; }; + /** @description Organization invite sent to an email address */ InviteDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Unique invite identifier + */ inviteId?: number; + /** @description Email address the invite was sent to */ email?: string; - /** @enum {string} */ + /** + * @description Role that will be assigned to the invitee on acceptance + * @enum {string} + */ roleOffered?: "OWNER" | "ADMIN" | "MEMBER"; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the invite expires + */ expiresAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the invite was accepted; null if not yet used + */ consumedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the invite was revoked; null if active + */ revokedAt?: string | null; }; SingleValueResponseInviteDto: { @@ -4782,9 +5560,14 @@ export interface components { data?: components["schemas"]["IncidentDetailDto"]; }; AddIncidentUpdateRequest: { + /** @description Update message or post-mortem notes */ body?: string; - /** @enum {string} */ + /** + * @description Updated incident status; null to keep current status + * @enum {string} + */ newStatus?: "WATCHING" | "TRIGGERED" | "CONFIRMED" | "RESOLVED"; + /** @description Whether to notify subscribers of this update */ notifySubscribers?: boolean; }; ResolveIncidentRequest: { @@ -4809,8 +5592,8 @@ export interface components { lockedBy: string; /** * Format: int32 - * @description Lock TTL in minutes. Defaults to 10. Max 60. - * @example 10 + * @description Lock TTL in minutes (default: 30, max: 60) + * @example 30 */ ttlMinutes?: number | null; }; @@ -4846,33 +5629,66 @@ export interface components { */ expiresAt?: string | null; }; + /** @description Created API key with the full key value — store it now, it won't be shown again */ ApiKeyCreateResponse: { - /** Format: int32 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the key was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the key expires; null if no expiration + */ expiresAt?: string | null; }; SingleValueResponseApiKeyCreateResponse: { data?: components["schemas"]["ApiKeyCreateResponse"]; }; + /** @description API key for programmatic access to the DevHelm API */ ApiKeyDto: { - /** Format: int32 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the key was created + */ createdAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the key was last updated + */ updatedAt?: string; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp of the most recent API call; null if never used + */ lastUsedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the key was revoked; null if active + */ revokedAt?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the key expires; null if no expiration + */ expiresAt?: string | null; }; SingleValueResponseApiKeyDto: { @@ -4893,6 +5709,7 @@ export interface components { success?: boolean; message?: string; }; + /** @description Alert channel configuration to test without saving */ TestAlertChannelRequest: { config: components["schemas"]["DiscordChannelConfig"] | components["schemas"]["EmailChannelConfig"] | components["schemas"]["OpsGenieChannelConfig"] | components["schemas"]["PagerDutyChannelConfig"] | components["schemas"]["SlackChannelConfig"] | components["schemas"]["TeamsChannelConfig"] | components["schemas"]["WebhookChannelConfig"]; }; @@ -4915,22 +5732,35 @@ export interface components { }; SingleValueResponseMapStringString: { data?: { - [key: string]: string; - }; + [key: string]: string | null; + } | null; }; SingleValueResponseListMonitorAssertionDto: { - data?: components["schemas"]["MonitorAssertionDto"][]; + data?: components["schemas"]["MonitorAssertionDto"][] | null; }; SchedulableMonitorDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique monitor identifier + */ id?: string; - /** @enum {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 */ + /** + * Format: int32 + * @description Check frequency in seconds + */ frequencySeconds?: number; + /** @description Probe regions to execute checks from */ regions?: string[]; - /** Format: int32 */ + /** + * Format: int32 + * @description Organization this monitor belongs to + */ organizationId?: number; }; TableValueResultAdapterHealthDto: { @@ -4939,7 +5769,7 @@ export interface components { hasPrev?: boolean; }; SingleValueResponseListBillingPlanDto: { - data?: components["schemas"]["BillingPlanDto"][]; + data?: components["schemas"]["BillingPlanDto"][] | null; }; TableValueResultTransactionDto: { data?: components["schemas"]["TransactionDto"][]; @@ -4954,14 +5784,27 @@ export interface components { SingleValueResponseUpcomingChargeResponse: { data?: components["schemas"]["UpcomingChargeResponse"]; }; + /** @description Preview of upcoming subscription charge after a plan change */ UpcomingChargeResponse: { - /** @enum {string} */ + /** + * @description Type of subscription action being previewed + * @enum {string} + */ action?: "UPGRADE" | "DOWNGRADE" | "NOOP"; - /** Format: int32 */ + /** + * Format: int32 + * @description Amount due immediately (proration) in smallest currency unit + */ immediateAmount?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Amount that will be charged on the next billing cycle + */ nextBillingAmount?: number; - /** Format: date-time */ + /** + * Format: date-time + * @description Date of the next billing cycle; null if cancelling + */ nextBillingDate?: string | null; }; /** @description A single resolved entitlement for the organization */ @@ -5018,9 +5861,14 @@ export interface components { /** Format: int32 */ size?: number; }; + /** @description Generic id/value pair for select options and autocomplete */ IdValuePair: { - /** Format: int32 */ + /** + * Format: int32 + * @description Numeric identifier + */ id?: number; + /** @description Display label or value */ value?: string; }; TableValueResultIdValuePair: { @@ -5028,13 +5876,24 @@ export interface components { hasNext?: boolean; hasPrev?: boolean; }; + /** @description Membership summary for an organization the user belongs to */ MyOrgItemDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Organization identifier + */ orgId?: number; + /** @description Organization name */ orgName?: string; - /** @enum {string} */ + /** + * @description Member role within this organization + * @enum {string} + */ orgRole?: "OWNER" | "ADMIN" | "MEMBER"; - /** @enum {string} */ + /** + * @description Membership status + * @enum {string} + */ status?: "INVITED" | "ACTIVE" | "SUSPENDED" | "LEFT" | "REMOVED" | "DECLINED"; }; TableValueResultMyOrgItemDto: { @@ -5122,6 +5981,7 @@ export interface components { configured?: boolean; maskedSecret?: string | null; }; + /** @description List of all available webhook event types */ WebhookEventCatalogEntry: { /** @description Dot-notation event type identifier, e.g. "monitor.created" */ type?: string; @@ -5131,6 +5991,7 @@ export interface components { description?: string; }; WebhookEventCatalogResponse: { + /** @description List of all available webhook event types */ data?: components["schemas"]["WebhookEventCatalogEntry"][]; }; /** @description Cursor-paginated response for time-series and append-only data */ @@ -5167,39 +6028,75 @@ export interface components { activeIncidentCount?: number; dataCompleteness?: string; }; + /** @description A component affected by a scheduled maintenance window */ MaintenanceComponentRef: { - /** Format: uuid */ + /** + * 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 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when this update was posted + */ displayAt?: string | null; }; /** @description A scheduled maintenance window from a vendor status page */ ScheduledMaintenanceDto: { - /** Format: uuid */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the maintenance is scheduled to begin + */ scheduledFor?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the maintenance is scheduled to end + */ scheduledUntil?: string | null; - /** Format: date-time */ + /** + * Format: date-time + * @description Timestamp when the maintenance actually started + */ startedAt?: string | null; - /** Format: date-time */ + /** + * 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"][]; }; ServiceDetailDto: { @@ -5349,16 +6246,29 @@ export interface components { }; /** @description Daily uptime data for a component */ ComponentUptimeDayDto: { - /** Format: date-time */ + /** + * Format: date-time + * @description Date of the daily bucket (ISO 8601) + */ date?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Seconds of partial outage observed on this day + */ partialOutageSeconds?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Seconds of major outage observed on this day + */ majorOutageSeconds?: number; - /** Format: double */ + /** + * Format: double + * @description Computed uptime percentage for the day + */ uptimePercentage?: number; /** @description Incident event references for this day as raw JSON */ eventsJson?: string | null; + /** @description Data source: vendor_reported or incident_derived */ source?: string; }; TableValueResultComponentUptimeDayDto: { @@ -5366,21 +6276,44 @@ export interface components { hasNext?: boolean; hasPrev?: boolean; }; + /** @description Global status summary across all subscribed vendor services */ GlobalStatusSummaryDto: { - /** Format: int32 */ + /** + * Format: int32 + * @description Total number of services in the catalog + */ totalServices?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of services currently fully operational + */ operationalCount?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of services with degraded status + */ degradedCount?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of services with partial outage + */ partialOutageCount?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of services with major outage + */ majorOutageCount?: number; - /** Format: int32 */ + /** + * Format: int32 + * @description Number of services currently under maintenance + */ maintenanceCount?: number; - /** Format: int64 */ + /** + * Format: int64 + * @description Total number of active incidents across all services + */ activeIncidentCount?: number; + /** @description Services that are not fully operational */ servicesWithIssues?: components["schemas"]["ServiceCatalogDto"][]; }; SingleValueResponseGlobalStatusSummaryDto: { @@ -5404,16 +6337,29 @@ export interface components { SingleValueResponseResourceGroupHealthDto: { data?: components["schemas"]["ResourceGroupHealthDto"]; }; + /** @description In-app notification for the current user */ NotificationDto: { - /** Format: int64 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the notification was created + */ createdAt?: string; }; TableValueResultNotificationDto: { @@ -5423,7 +6369,7 @@ export interface components { }; SingleValueResponseLong: { /** Format: int64 */ - data?: number; + data?: number | null; }; TableValueResultNotificationPolicyDto: { data?: components["schemas"]["NotificationPolicyDto"][]; @@ -5440,20 +6386,40 @@ export interface components { hasNext?: boolean; hasPrev?: boolean; }; + /** @description A point-in-time version snapshot of a monitor configuration */ MonitorVersionDto: { - /** Format: uuid */ + /** + * Format: uuid + * @description Unique version record identifier + */ id?: string; - /** Format: uuid */ + /** + * Format: uuid + * @description Monitor this version belongs to + */ monitorId?: string; - /** Format: int32 */ + /** + * Format: int32 + * @description Monotonically increasing version number + */ version?: number; snapshot?: components["schemas"]["MonitorDto"]; - /** Format: int32 */ + /** + * Format: int32 + * @description User ID who made the change; null for automated changes + */ changedById?: number | null; - /** @enum {string} */ + /** + * @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 */ + /** + * Format: date-time + * @description Timestamp when this version was recorded + */ createdAt?: string; }; TableValueResultMonitorVersionDto: { @@ -5934,10 +6900,12 @@ export interface components { hasNext?: boolean; hasPrev?: boolean; }; + /** @description Combined dashboard overview for monitors and incidents */ DashboardOverviewDto: { monitors?: components["schemas"]["MonitorsSummaryDto"]; incidents?: components["schemas"]["IncidentsSummaryDto"]; }; + /** @description Incident summary counters */ IncidentsSummaryDto: { /** Format: int64 */ active?: number; @@ -5946,28 +6914,55 @@ export interface components { /** Format: double */ mttr30d?: number | null; }; + /** @description Dashboard summary counters for monitors */ MonitorsSummaryDto: { - /** Format: int64 */ + /** + * Format: int64 + * @description Total number of monitors in the organization + */ total?: number; - /** Format: int64 */ + /** + * Format: int64 + * @description Number of monitors currently passing + */ up?: number; - /** Format: int64 */ + /** + * Format: int64 + * @description Number of monitors currently failing (DOWN severity) + */ down?: number; - /** Format: int64 */ + /** + * Format: int64 + * @description Number of monitors with degraded status + */ degraded?: number; - /** Format: int64 */ + /** + * Format: int64 + * @description Number of disabled monitors + */ paused?: number; - /** Format: double */ + /** + * Format: double + * @description Average uptime percentage across all monitors over last 24h + */ avgUptime24h?: number | null; - /** Format: double */ + /** + * Format: double + * @description Average uptime percentage across all monitors over last 30 days + */ avgUptime30d?: number | null; }; SingleValueResponseDashboardOverviewDto: { data?: components["schemas"]["DashboardOverviewDto"]; }; + /** @description Service category with its count of catalog entries */ CategoryDto: { + /** @description Category name (e.g. CI/CD, Cloud, Payments) */ category?: string; - /** Format: int64 */ + /** + * Format: int64 + * @description Number of services in this category + */ serviceCount?: number; }; TableValueResultCategoryDto: { @@ -6064,19 +7059,34 @@ export interface components { data?: components["schemas"]["AuthMeResponse"]; }; AuditEventDto: { - /** Format: int64 */ + /** + * Format: int64 + * @description Unique audit event identifier + */ id?: number; - /** Format: int32 */ + /** + * 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 */ + /** + * Format: date-time + * @description Timestamp when the action was performed + */ createdAt?: string; }; PageResultAuditEventDto: { diff --git a/src/lib/descriptions.generated.ts b/src/lib/descriptions.generated.ts index 7b5d48d..7c50d82 100644 --- a/src/lib/descriptions.generated.ts +++ b/src/lib/descriptions.generated.ts @@ -129,6 +129,10 @@ export const fieldDescriptions: Record> = "type": "Monitor protocol type to test", "assertions": "Optional assertions to evaluate against the test result" }, + "AcquireDeployLockRequest": { + "lockedBy": "Identity of the lock requester (e.g. hostname, CI job ID)", + "ttlMinutes": "Lock TTL in minutes (default: 30, max: 60)" + }, "HttpMonitorConfig": { "url": "Target URL to send requests to", "method": "HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD", From d2ebd360c4b21f45717d18643dfa40cfc9ba0e66 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 16:11:06 +0200 Subject: [PATCH 5/7] fix: update unit tests to match handler changes - Environment handler now uses slug for update/delete paths (not ID) - Webhook snapshot now includes `enabled` field - Remove dead resourceGroup validation test (field was removed) Made-with: Cursor --- test/yaml/applier.test.ts | 6 +++--- test/yaml/differ.test.ts | 6 +++--- test/yaml/handlers.test.ts | 4 ++-- test/yaml/idempotency.test.ts | 4 ++-- test/yaml/validator.test.ts | 12 ------------ 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/test/yaml/applier.test.ts b/test/yaml/applier.test.ts index 3e7a729..0d9bb17 100644 --- a/test/yaml/applier.test.ts +++ b/test/yaml/applier.test.ts @@ -256,7 +256,7 @@ describe('applier', () => { const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) expect(mockPut).toHaveBeenCalledWith('/api/v1/environments/{slug}', { - params: {path: {slug: 'env-42'}}, + params: {path: {slug: 'prod'}}, body: {name: 'Prod', variables: {KEY: 'val'}, isDefault: undefined}, }) }) @@ -336,7 +336,7 @@ describe('applier', () => { expect(result.succeeded).toHaveLength(1) expect(mockPut).toHaveBeenCalledWith('/api/v1/webhooks/{id}', { params: {path: {id: 'wh-1'}}, - body: {url: 'https://hook.com', subscribedEvents: ['monitor.up'], description: undefined}, + body: {url: 'https://hook.com', subscribedEvents: ['monitor.up'], description: undefined, enabled: null}, }) }) @@ -417,7 +417,7 @@ describe('applier', () => { } const result = await apply(changeset, emptyRefs(), fakeClient) expect(result.succeeded).toHaveLength(1) - expect(mockDelete).toHaveBeenCalledWith('/api/v1/environments/env-7', expect.anything()) + expect(mockDelete).toHaveBeenCalledWith('/api/v1/environments/stg', expect.anything()) }) it('deletes a secret', async () => { diff --git a/test/yaml/differ.test.ts b/test/yaml/differ.test.ts index d73b71c..44cf001 100644 --- a/test/yaml/differ.test.ts +++ b/test/yaml/differ.test.ts @@ -114,7 +114,7 @@ describe('differ', () => { it('skips update when webhook unchanged', () => { const refs = new ResolvedRefs() refs.set('webhooks', 'https://hooks.com/x', {id: 'wh-1', refKey: 'https://hooks.com/x', raw: { - url: 'https://hooks.com/x', subscribedEvents: ['monitor.down', 'monitor.recovered'], description: 'test', + url: 'https://hooks.com/x', subscribedEvents: ['monitor.down', 'monitor.recovered'], description: 'test', enabled: true, }}) const config: DevhelmConfig = { webhooks: [{url: 'https://hooks.com/x', events: ['monitor.down', 'monitor.recovered'], description: 'test'}], @@ -313,7 +313,7 @@ describe('differ', () => { it('webhook events order does not matter', () => { const refs = new ResolvedRefs() refs.set('webhooks', 'https://x.com', {id: 'wh-1', refKey: 'https://x.com', raw: { - url: 'https://x.com', subscribedEvents: ['b', 'a'], + url: 'https://x.com', subscribedEvents: ['b', 'a'], enabled: true, }}) const config: DevhelmConfig = { webhooks: [{url: 'https://x.com', events: ['a', 'b']}], @@ -354,7 +354,7 @@ describe('differ', () => { refs.set('tags', 'unchanged', {id: 'tag-1', refKey: 'unchanged', raw: {name: 'unchanged', color: '#FF0000'}}) refs.set('tags', 'changed', {id: 'tag-2', refKey: 'changed', raw: {name: 'changed', color: '#000000'}}) refs.set('webhooks', 'https://same.com', {id: 'wh-1', refKey: 'https://same.com', raw: { - url: 'https://same.com', subscribedEvents: ['a'], description: 'same', + url: 'https://same.com', subscribedEvents: ['a'], description: 'same', enabled: true, }}) const config: DevhelmConfig = { tags: [ diff --git a/test/yaml/handlers.test.ts b/test/yaml/handlers.test.ts index 688c756..d6e3728 100644 --- a/test/yaml/handlers.test.ts +++ b/test/yaml/handlers.test.ts @@ -117,7 +117,7 @@ describe('handler getApiRefKey + getApiId', () => { describe('handler deletePath', () => { it.each([ ['tag', '/api/v1/tags/id-1'], - ['environment', '/api/v1/environments/id-1'], + ['environment', '/api/v1/environments/ref-1'], ['secret', '/api/v1/secrets/id-1'], ['alertChannel', '/api/v1/alert-channels/id-1'], ['notificationPolicy', '/api/v1/notification-policies/id-1'], @@ -126,6 +126,6 @@ describe('handler deletePath', () => { ['monitor', '/api/v1/monitors/id-1'], ['dependency', '/api/v1/service-subscriptions/id-1'], ] as const)('%s → %s', (type, expectedPath) => { - expect(getHandler(type).deletePath('id-1')).toBe(expectedPath) + expect(getHandler(type).deletePath('id-1', 'ref-1')).toBe(expectedPath) }) }) diff --git a/test/yaml/idempotency.test.ts b/test/yaml/idempotency.test.ts index 041281b..203056a 100644 --- a/test/yaml/idempotency.test.ts +++ b/test/yaml/idempotency.test.ts @@ -160,7 +160,7 @@ describe('idempotency', () => { const refs = buildRefs([ {type: 'webhooks', key: 'https://example.com/webhook', id: 'w1', raw: { url: 'https://example.com/webhook', subscribedEvents: ['monitor.down', 'monitor.up'], - description: null, + description: null, enabled: true, }}, ]) const cs = diff(config, refs) @@ -224,7 +224,7 @@ describe('idempotency', () => { {type: 'secrets', key: 'TOKEN', id: 's1', raw: {key: 'TOKEN', valueHash: sha256Hex(secretValue)}}, {type: 'alertChannels', key: 'Slack', id: 'ac1', raw: {name: 'Slack', channelType: 'slack', configHash: channelHash}}, {type: 'webhooks', key: 'https://example.com/hook', id: 'w1', raw: { - url: 'https://example.com/hook', subscribedEvents: ['monitor.down'], description: null, + url: 'https://example.com/hook', subscribedEvents: ['monitor.down'], description: null, enabled: true, }}, {type: 'monitors', key: 'API', id: 'm1', raw: { name: 'API', type: 'HTTP', config: {url: 'https://api.example.com', method: 'GET'}, diff --git a/test/yaml/validator.test.ts b/test/yaml/validator.test.ts index c1746dc..5cae17b 100644 --- a/test/yaml/validator.test.ts +++ b/test/yaml/validator.test.ts @@ -492,18 +492,6 @@ describe('validator', () => { expect(result.warnings.some((w) => w.message.includes('missing-channel'))).toBe(true) }) - it('warns on unresolved resourceGroup reference in monitor', () => { - const config: DevhelmConfig = { - monitors: [{ - name: 'test', type: 'HTTP', - config: {url: 'https://x.com', method: 'GET'}, - resourceGroup: 'nonexistent-group', - }], - } - const result = validate(config) - expect(result.warnings.some((w) => w.message.includes('nonexistent-group'))).toBe(true) - }) - it('warns on unresolved service ref in resource group', () => { const config: DevhelmConfig = { resourceGroups: [{name: 'test', services: ['unknown-service']}], From ba45673360d0a6cfd0755b486e05d827365ec233 Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 16:20:24 +0200 Subject: [PATCH 6/7] CLI minor fixes --- src/commands/deploy/index.ts | 7 ++++++- src/commands/plan.ts | 7 ++++++- src/lib/yaml/differ.ts | 4 ++-- src/lib/yaml/entitlements.ts | 4 +++- src/lib/yaml/handlers.ts | 1 + src/lib/yaml/parser.ts | 7 ++----- src/lib/yaml/state.ts | 4 ++-- src/lib/yaml/types.ts | 1 + test/yaml/differ.test.ts | 21 +++++++++++++++++++++ 9 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts index f9492b5..b443f89 100644 --- a/src/commands/deploy/index.ts +++ b/src/commands/deploy/index.ts @@ -16,6 +16,7 @@ export default class Deploy extends Command { '<%= config.bin %> deploy --yes', '<%= config.bin %> deploy -f monitors.yml', '<%= config.bin %> deploy --prune --yes', + '<%= config.bin %> deploy --prune-all --yes', '<%= config.bin %> deploy --dry-run', '<%= config.bin %> deploy --dry-run --detailed-exitcode', '<%= config.bin %> deploy -o json --yes', @@ -37,6 +38,10 @@ export default class Deploy extends Command { description: 'Delete CLI-managed resources not present in config', default: false, }), + 'prune-all': Flags.boolean({ + description: 'Delete ALL resources not in config, including those not managed by the CLI (use with caution)', + default: false, + }), 'dry-run': Flags.boolean({ description: 'Show what would change without applying (same as "devhelm plan")', default: false, @@ -102,7 +107,7 @@ export default class Deploy extends Command { if (!isJson) this.log('Fetching current state from API...') const refs = await fetchAllRefs(client) - const changeset = diff(config, refs, {prune: flags.prune}) + const changeset = diff(config, refs, {prune: flags.prune || flags['prune-all'], pruneAll: flags['prune-all']}) const entitlementCheck = await checkEntitlements(client, changeset) diff --git a/src/commands/plan.ts b/src/commands/plan.ts index 2c6dd44..bb22566 100644 --- a/src/commands/plan.ts +++ b/src/commands/plan.ts @@ -12,6 +12,7 @@ export default class Plan extends Command { '<%= config.bin %> plan', '<%= config.bin %> plan -f monitors.yml', '<%= config.bin %> plan --prune', + '<%= config.bin %> plan --prune-all', '<%= config.bin %> plan --detailed-exitcode', '<%= config.bin %> plan -o json', ] @@ -27,6 +28,10 @@ export default class Plan extends Command { description: 'Include deletions of CLI-managed resources not in config', default: false, }), + 'prune-all': Flags.boolean({ + description: 'Include deletions of ALL resources not in config, including those not managed by the CLI', + default: false, + }), 'detailed-exitcode': Flags.boolean({ description: 'Return exit code 10 if plan has changes (for CI)', default: false, @@ -75,7 +80,7 @@ export default class Plan extends Command { this.log('Fetching current state from API...') const refs = await fetchAllRefs(client) - const changeset = diff(config, refs, {prune: flags.prune}) + const changeset = diff(config, refs, {prune: flags.prune || flags['prune-all'], pruneAll: flags['prune-all']}) const entitlementCheck = await checkEntitlements(client, changeset) diff --git a/src/lib/yaml/differ.ts b/src/lib/yaml/differ.ts index 59dd40e..181849c 100644 --- a/src/lib/yaml/differ.ts +++ b/src/lib/yaml/differ.ts @@ -78,10 +78,10 @@ function diffSection( } } - if (options.prune && items !== undefined) { + if ((options.prune || options.pruneAll) && items !== undefined) { for (const entry of refs.allEntries(handler.refType)) { if (!desired.has(entry.refKey)) { - if (handler.resourceType === 'monitor' && entry.managedBy !== 'CLI') continue + if (handler.resourceType === 'monitor' && !options.pruneAll && entry.managedBy !== 'CLI') continue deletes.push({ action: 'delete', resourceType: handler.resourceType, diff --git a/src/lib/yaml/entitlements.ts b/src/lib/yaml/entitlements.ts index a95d2cc..2e5232e 100644 --- a/src/lib/yaml/entitlements.ts +++ b/src/lib/yaml/entitlements.ts @@ -46,7 +46,9 @@ export async function checkEntitlements( try { const resp = await checkedFetch<{data?: AuthMeResponse}>(client.GET('/api/v1/auth/me')) data = resp.data ?? {} - } catch { + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + process.stderr.write(`Entitlement check skipped: ${msg}\n`) return null } diff --git a/src/lib/yaml/handlers.ts b/src/lib/yaml/handlers.ts index 2f35dde..08821e8 100644 --- a/src/lib/yaml/handlers.ts +++ b/src/lib/yaml/handlers.ts @@ -664,6 +664,7 @@ export const HANDLER_MAP: Record = { dependency: dependencyHandler, } +/** @internal – used by tests to look up a handler by resource type */ export function getHandler(type: HandledResourceType): ResourceHandler { return HANDLER_MAP[type] } diff --git a/src/lib/yaml/parser.ts b/src/lib/yaml/parser.ts index e7a689e..619e93a 100644 --- a/src/lib/yaml/parser.ts +++ b/src/lib/yaml/parser.ts @@ -121,11 +121,8 @@ function mergeConfigs(configs: DevhelmConfig[]): DevhelmConfig { for (const key of YAML_SECTION_KEYS) { const items = cfg[key] if (items) { - // TypeScript can't narrow cfg[key] through a generic dynamic index on a - // heterogeneous interface. YAML_SECTION_KEYS guarantees key ∈ DevhelmConfig - // and every value is T[] | undefined, so the concat is safe. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(merged as any)[key] = [...((merged as any)[key] ?? []), ...items] + const dest = (merged as Record) + dest[key] = [...(dest[key] ?? []), ...items] } } } diff --git a/src/lib/yaml/state.ts b/src/lib/yaml/state.ts index 49dce0a..5aa78bf 100644 --- a/src/lib/yaml/state.ts +++ b/src/lib/yaml/state.ts @@ -1,6 +1,6 @@ /** - * Local state file for tracking which resources were created by `devhelm deploy`. - * Used for pruning: only delete resources that we manage. + * Local state file for tracking resources managed by `devhelm deploy`. + * Reserved for future use (e.g. offline drift detection, state-aware pruning). * * State file: .devhelm/state.json (gitignored by convention) */ diff --git a/src/lib/yaml/types.ts b/src/lib/yaml/types.ts index fa9e09f..b0fea0b 100644 --- a/src/lib/yaml/types.ts +++ b/src/lib/yaml/types.ts @@ -47,6 +47,7 @@ export interface Change { export interface DiffOptions { prune?: boolean + pruneAll?: boolean } export interface Changeset { diff --git a/test/yaml/differ.test.ts b/test/yaml/differ.test.ts index 44cf001..0076d45 100644 --- a/test/yaml/differ.test.ts +++ b/test/yaml/differ.test.ts @@ -222,6 +222,27 @@ describe('differ', () => { expect(changeset.deletes).toHaveLength(0) }) + it('pruneAll deletes non-CLI monitors', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'dashboard-monitor', { + id: 'mon-1', refKey: 'dashboard-monitor', managedBy: 'DASHBOARD', raw: {managedBy: 'DASHBOARD'}, + }) + const config: DevhelmConfig = {monitors: []} + const changeset = diff(config, refs, {prune: true, pruneAll: true}) + expect(changeset.deletes).toHaveLength(1) + expect(changeset.deletes[0].refKey).toBe('dashboard-monitor') + }) + + it('pruneAll deletes monitors with no managedBy', () => { + const refs = new ResolvedRefs() + refs.set('monitors', 'orphan', { + id: 'mon-1', refKey: 'orphan', raw: {}, + }) + const config: DevhelmConfig = {monitors: []} + const changeset = diff(config, refs, {prune: true, pruneAll: true}) + expect(changeset.deletes).toHaveLength(1) + }) + it('prune: omitted section (undefined) does not delete', () => { const refs = new ResolvedRefs() refs.set('tags', 'T', {id: '1', refKey: 'T', raw: {name: 'T'}}) From aebc633548863404d66b886ae2ad93cb45ec5daa Mon Sep 17 00:00:00 2001 From: caballeto Date: Sat, 11 Apr 2026 16:51:31 +0200 Subject: [PATCH 7/7] Fixes --- docs/openapi/monitoring-api.json | 17189 +--------------- .../monitors/versions/{index.ts => list.ts} | 6 +- src/lib/api.generated.ts | 55 +- src/lib/descriptions.generated.ts | 6 +- 4 files changed, 46 insertions(+), 17210 deletions(-) rename src/commands/monitors/versions/{index.ts => list.ts} (88%) diff --git a/docs/openapi/monitoring-api.json b/docs/openapi/monitoring-api.json index 9dd3089..0a78680 100644 --- a/docs/openapi/monitoring-api.json +++ b/docs/openapi/monitoring-api.json @@ -1,17188 +1 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "DevHelm API", - "description": "DevHelm platform and public API", - "version": "1.0" - }, - "servers": [ - { - "url": "http://localhost:8080", - "description": "Generated server url" - } - ], - "tags": [ - { - "name": "Heartbeat", - "description": "Public ping endpoint for heartbeat monitors" - }, - { - "name": "Invites", - "description": "Organization invite management" - }, - { - "name": "Onboarding", - "description": "User onboarding flow" - }, - { - "name": "Members", - "description": "Organization member management" - }, - { - "name": "Me", - "description": "Current user profile and organizations" - }, - { - "name": "Incidents", - "description": "Incident management and lifecycle" - }, - { - "name": "Maintenance Windows", - "description": "Schedule alert-suppression windows for monitors" - }, - { - "name": "Organizations", - "description": "Organization management" - }, - { - "name": "Integrations", - "description": "Static catalog of supported alert channel integrations" - }, - { - "name": "Incident Policies", - "description": "Manage trigger, confirmation, and recovery rules for monitors" - }, - { - "name": "Entitlements", - "description": "Plan entitlements and usage limits" - }, - { - "name": "Vault", - "description": "Organization vault management (admin-only)" - }, - { - "name": "Secrets", - "description": "Organization environment secret management" - }, - { - "name": "Transactions", - "description": "Subscription transaction history" - }, - { - "name": "Monitors", - "description": "Monitor CRUD and lifecycle management" - }, - { - "name": "Webhooks", - "description": "Webhook endpoint management, event catalog, and delivery history" - }, - { - "name": "Events", - "description": "Real-time event stream" - }, - { - "name": "Workspaces", - "description": "Workspace management within an organization" - }, - { - "name": "Notifications", - "description": "In-app notification center" - }, - { - "name": "Alert Channels", - "description": "Alert channel CRUD and connectivity testing" - }, - { - "name": "Subscriptions", - "description": "Organization subscription management" - }, - { - "name": "Service Subscriptions", - "description": "Manage which services an organization tracks" - }, - { - "name": "Tags", - "description": "Org-scoped tag management for monitors" - }, - { - "name": "Status Data", - "description": "Public service status catalog, components, uptime, and incident history" - }, - { - "name": "Check Results", - "description": "Query raw check results, uptime statistics, and summary data" - }, - { - "name": "API Keys", - "description": "Organization API key management" - }, - { - "name": "Dashboard", - "description": "Overview dashboard aggregates" - }, - { - "name": "Auth", - "description": "User registration" - }, - { - "name": "Monitor Auth", - "description": "Manage authentication configuration for a monitor" - }, - { - "name": "Audit Log", - "description": "Organization audit trail" - }, - { - "name": "Monitor Alert Channels", - "description": "Manage alert channel mappings for a monitor" - }, - { - "name": "Alert Deliveries", - "description": "Delivery audit trail: inspect per-attempt details for alert deliveries" - }, - { - "name": "API Auth", - "description": "Identity and quota info for API key authentication" - }, - { - "name": "Resource Groups", - "description": "Resource group CRUD and member management" - }, - { - "name": "Notification Policies", - "description": "Org-level notification routing policies with JSONB match rules" - }, - { - "name": "Notification Dispatches", - "description": "Dispatch debugging API: inspect which policies matched an incident and track delivery status" - }, - { - "name": "Environments", - "description": "Variable namespace management for monitors" - }, - { - "name": "Monitor Assertions", - "description": "Manage assertions for a monitor" - }, - { - "name": "Deploy Lock", - "description": "Mutex for CLI deploy operations" - }, - { - "name": "Billing", - "description": "Billing plans and pricing" - } - ], - "paths": { - "/platform/orgs/{orgId}/subscriptions/{subscriptionId}": { - "put": { - "tags": [ - "Subscriptions" - ], - "summary": "Update subscription", - "operationId": "updateSubscription", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "subscriptionId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSubscriptionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseSubscriptionDto" - } - } - } - } - } - } - }, - "/platform/onboarding/orgs/{orgId}/details": { - "put": { - "tags": [ - "Onboarding" - ], - "summary": "Update organization details", - "operationId": "updateOrgDetails", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgDetailsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" - } - } - } - } - } - } - }, - "/platform/onboarding/advance": { - "put": { - "tags": [ - "Onboarding" - ], - "summary": "Advance onboarding stage forward", - "operationId": "advanceStage", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOnboardingStageRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - } - }, - "/platform/me": { - "get": { - "tags": [ - "Me" - ], - "summary": "Get current user", - "operationId": "me", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Me" - ], - "summary": "Update current user profile", - "operationId": "updateProfile", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateProfileRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - } - }, - "/platform/me/notification-preferences": { - "get": { - "tags": [ - "Me" - ], - "summary": "Get current user's notification preferences", - "operationId": "getNotificationPreferences", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPreferencesDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Me" - ], - "summary": "Update current user's notification preferences", - "operationId": "updateNotificationPreferences", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateNotificationPreferencesRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPreferencesDto" - } - } - } - } - } - } - }, - "/platform/admin/workspaces/{workspaceId}": { - "get": { - "tags": [ - "admin-workspace-controller" - ], - "operationId": "getWorkspace", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "admin-workspace-controller" - ], - "operationId": "updateWorkspace", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWorkspaceRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "admin-workspace-controller" - ], - "operationId": "deleteWorkspace", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/platform/admin/users/{userId}": { - "put": { - "tags": [ - "admin-controller" - ], - "operationId": "updateUser", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateUserRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - } - }, - "/platform/admin/orgs/{orgId}": { - "put": { - "tags": [ - "admin-controller" - ], - "operationId": "updateOrganization", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgDetailsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" - } - } - } - } - } - } - }, - "/platform/admin/orgs/{orgId}/members/{userId}/role": { - "put": { - "tags": [ - "admin-member-controller" - ], - "operationId": "updateMemberRole", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangeRoleRequest" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/workspaces/{workspaceId}": { - "get": { - "tags": [ - "Workspaces" - ], - "summary": "Get workspace by ID", - "operationId": "get", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Workspaces" - ], - "summary": "Update workspace", - "operationId": "update", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWorkspaceRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Workspaces" - ], - "summary": "Delete workspace", - "operationId": "delete", - "parameters": [ - { - "name": "workspaceId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/webhooks/{id}": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Get a single webhook endpoint", - "operationId": "get_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Webhooks" - ], - "summary": "Update a webhook endpoint", - "operationId": "update_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWebhookEndpointRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Webhooks" - ], - "summary": "Delete a webhook endpoint", - "operationId": "delete_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/tags/{id}": { - "put": { - "tags": [ - "Tags" - ], - "summary": "Update a tag's name and/or color", - "operationId": "update_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTagRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTagDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Tags" - ], - "summary": "Delete a tag (cascades to all monitor associations)", - "operationId": "delete_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/secrets/{key}": { - "put": { - "tags": [ - "Secrets" - ], - "summary": "Update secret", - "operationId": "update_3", - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateSecretRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseSecretDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Secrets" - ], - "summary": "Delete secret", - "operationId": "delete_3", - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/resource-groups/{id}": { - "get": { - "tags": [ - "Resource Groups" - ], - "summary": "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.", - "operationId": "get_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "includeMetrics", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Resource Groups" - ], - "summary": "Update a resource group's name, description, alert policy, inherited settings, and health threshold", - "operationId": "update_4", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateResourceGroupRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Resource Groups" - ], - "summary": "Delete a resource group (cascades to member rows)", - "operationId": "delete_4", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/org": { - "get": { - "tags": [ - "Organizations" - ], - "summary": "Get the current organization", - "operationId": "get_3", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Organizations" - ], - "summary": "Update the current organization", - "operationId": "update_5", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateOrgDetailsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" - } - } - } - } - } - } - }, - "/api/v1/notifications/{id}/read": { - "put": { - "tags": [ - "Notifications" - ], - "summary": "Mark a notification as read", - "operationId": "markRead", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/notifications/read-all": { - "put": { - "tags": [ - "Notifications" - ], - "summary": "Mark all notifications as read", - "operationId": "markAllRead", - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/notification-policies/{id}": { - "get": { - "tags": [ - "Notification Policies" - ], - "summary": "Get a notification policy by ID", - "operationId": "getById", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Notification Policies" - ], - "summary": "Update a notification policy", - "operationId": "update_6", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateNotificationPolicyRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Notification Policies" - ], - "summary": "Delete a notification policy", - "operationId": "delete_5", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{monitorId}/policy": { - "get": { - "tags": [ - "Incident Policies" - ], - "summary": "Get incident policy for a monitor", - "description": "Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.", - "operationId": "get_4", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "description": "Monitor UUID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "Policy found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/IncidentPolicyDto" - } - } - } - }, - "404": { - "description": "Monitor or policy not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Incident Policies" - ], - "summary": "Update incident policy for a monitor", - "description": "Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.", - "operationId": "update_7", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "description": "Monitor UUID", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Policy updated", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/IncidentPolicyDto" - } - } - } - }, - "400": { - "description": "Validation error in JSONB shape", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" - } - } - } - }, - "404": { - "description": "Monitor or policy not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{monitorId}/auth": { - "put": { - "tags": [ - "Monitor Auth" - ], - "summary": "Update authentication config for a monitor", - "operationId": "update_8", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMonitorAuthRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Monitor Auth" - ], - "summary": "Set authentication config for a monitor", - "operationId": "set", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetMonitorAuthRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Monitor Auth" - ], - "summary": "Remove authentication config from a monitor", - "operationId": "remove", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{monitorId}/assertions/{assertionId}": { - "put": { - "tags": [ - "Monitor Assertions" - ], - "summary": "Update an assertion on a monitor", - "operationId": "update_9", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "assertionId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAssertionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Monitor Assertions" - ], - "summary": "Remove an assertion from a monitor", - "operationId": "remove_1", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "assertionId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{monitorId}/alert-channels": { - "put": { - "tags": [ - "Monitor Alert Channels" - ], - "summary": "Replace the linked alert channel set for a monitor", - "operationId": "setChannels", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetAlertChannelsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseListUUID" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "Get a single monitor by id", - "operationId": "get_5", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Monitors" - ], - "summary": "Update a monitor", - "operationId": "update_10", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMonitorRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Monitors" - ], - "summary": "Soft-delete a monitor", - "operationId": "delete_6", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/members/{userId}/status": { - "put": { - "tags": [ - "Members" - ], - "summary": "Change member status", - "operationId": "changeStatus", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangeStatusRequest" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/members/{userId}/role": { - "put": { - "tags": [ - "Members" - ], - "summary": "Change member role", - "operationId": "changeRole", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChangeRoleRequest" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/maintenance-windows/{id}": { - "get": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Get a single maintenance window by ID", - "operationId": "getById_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Update a maintenance window", - "operationId": "update_11", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMaintenanceWindowRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Maintenance Windows" - ], - "summary": "Delete a maintenance window", - "operationId": "delete_7", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/environments/{slug}": { - "get": { - "tags": [ - "Environments" - ], - "summary": "Get environment by slug", - "operationId": "get_6", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" - } - } - } - } - } - }, - "put": { - "tags": [ - "Environments" - ], - "summary": "Update environment", - "operationId": "update_12", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateEnvironmentRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Environments" - ], - "summary": "Delete environment", - "operationId": "delete_8", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/alert-channels/{id}": { - "put": { - "tags": [ - "Alert Channels" - ], - "summary": "Update an alert channel's name and re-encrypt config", - "operationId": "update_13", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAlertChannelRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAlertChannelDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Alert Channels" - ], - "summary": "Soft-delete an alert channel and return affected policy summary", - "operationId": "delete_9", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/DeleteChannelResult" - } - } - } - } - } - } - }, - "/v1/webhooks/paddle": { - "post": { - "tags": [ - "paddle-webhook-controller" - ], - "operationId": "handleWebhook", - "parameters": [ - { - "name": "paddle-signature", - "in": "header", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/internal/workspaces": { - "post": { - "tags": [ - "workspaces-controller" - ], - "operationId": "create", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkspaceCreateParams" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - } - }, - "/v1/internal/service-incidents": { - "post": { - "tags": [ - "service-incident-internal-controller" - ], - "operationId": "createOrResolve", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceIncidentRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultIncidentDto" - } - } - } - } - } - } - }, - "/v1/internal/resource-groups/services/{serviceId}/re-evaluate-health": { - "post": { - "tags": [ - "resource-groups-internal-controller" - ], - "operationId": "reEvaluateGroupHealthForService", - "parameters": [ - { - "name": "serviceId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInteger" - } - } - } - } - } - } - }, - "/v1/internal/resource-groups/monitors/{monitorId}/re-evaluate-health": { - "post": { - "tags": [ - "resource-groups-internal-controller" - ], - "operationId": "reEvaluateGroupHealthForMonitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInteger" - } - } - } - } - } - } - }, - "/v1/internal/incidents": { - "post": { - "tags": [ - "incidents-internal-controller" - ], - "operationId": "createAutoIncident", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAutoIncidentRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDto" - } - } - } - } - } - } - }, - "/v1/internal/incidents/{id}/resolve": { - "post": { - "tags": [ - "incidents-internal-controller" - ], - "operationId": "resolveAutoIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDto" - } - } - } - } - } - } - }, - "/v1/internal/incidents/{id}/reopen": { - "post": { - "tags": [ - "incidents-internal-controller" - ], - "operationId": "reopenAutoIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReopenAutoIncidentRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDto" - } - } - } - } - } - } - }, - "/v1/internal/escalation-tick": { - "post": { - "tags": [ - "escalation-internal-controller" - ], - "operationId": "runEscalationTick", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInteger" - } - } - } - } - } - } - }, - "/v1/internal/billing/sync": { - "post": { - "tags": [ - "admin-billing-controller" - ], - "operationId": "syncFromPaddle", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInteger" - } - } - } - } - } - } - }, - "/v1/internal/adapters/health": { - "get": { - "tags": [ - "adapter-health-internal-controller" - ], - "operationId": "getAllHealth", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultAdapterHealthDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "adapter-health-internal-controller" - ], - "operationId": "reportOutcome", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AdapterHealthReportRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAdapterHealthDto" - } - } - } - } - } - } - }, - "/platform/orgs": { - "post": { - "tags": [ - "Organizations" - ], - "summary": "Create organization", - "operationId": "create_1", - "parameters": [ - { - "name": "ifNotExists", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": false - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateOrgRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseOrganizationDto" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}/transactions": { - "get": { - "tags": [ - "Transactions" - ], - "summary": "List transactions", - "operationId": "list", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "maximum": 100, - "minimum": 1, - "type": "integer", - "format": "int32", - "default": 10 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultTransactionDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Transactions" - ], - "summary": "Create subscription transaction", - "operationId": "createSubscriptionTransaction", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSubscriptionRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTransactionDto" - } - } - } - } - } - } - }, - "/platform/onboarding/quick-monitor": { - "post": { - "tags": [ - "Onboarding" - ], - "summary": "Create a monitor with smart defaults from URL analysis", - "operationId": "quickMonitor", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QuickMonitorRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - } - }, - "/platform/onboarding/complete-setup": { - "post": { - "tags": [ - "Onboarding" - ], - "summary": "Complete onboarding setup (creates org + workspace, advances to FIRST_MONITOR)", - "operationId": "completeSetup", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OnboardingSetupRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - } - }, - "/platform/onboarding/analyze-url": { - "post": { - "tags": [ - "Onboarding" - ], - "summary": "Analyze a URL and return suggested monitor configuration", - "operationId": "analyzeUrl", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AnalyzeUrlRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAnalyzeUrlResponse" - } - } - } - } - } - } - }, - "/platform/invites/accept": { - "post": { - "tags": [ - "Invites" - ], - "summary": "Accept invite", - "operationId": "accept", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcceptInviteRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAcceptInviteDto" - } - } - } - } - } - } - }, - "/platform/auth/register": { - "post": { - "tags": [ - "Auth" - ], - "summary": "Register user", - "operationId": "register", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterUserRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUserDto" - } - } - } - } - } - } - }, - "/platform/admin/orgs/{orgId}/workspaces": { - "get": { - "tags": [ - "admin-workspace-controller" - ], - "operationId": "listWorkspaces", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultWorkspaceDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "admin-workspace-controller" - ], - "operationId": "createWorkspace", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - } - }, - "/platform/admin/orgs/{orgId}/members": { - "get": { - "tags": [ - "admin-member-controller" - ], - "operationId": "listMembers", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMemberDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "admin-member-controller" - ], - "operationId": "addMember", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddMemberRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMemberDto" - } - } - } - } - } - } - }, - "/platform/admin/adapters/{serviceId}/enable": { - "post": { - "tags": [ - "admin-adapter-health-controller" - ], - "operationId": "reEnableAdapter", - "parameters": [ - { - "name": "serviceId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAdapterHealthDto" - } - } - } - } - } - } - }, - "/api/v1/workspaces": { - "get": { - "tags": [ - "Workspaces" - ], - "summary": "List workspaces", - "operationId": "list_1", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultWorkspaceDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Workspaces" - ], - "summary": "Create workspace", - "operationId": "create_2", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWorkspaceRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - } - }, - "/api/v1/webhooks": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List webhook endpoints for the authenticated org", - "operationId": "list_2", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultWebhookEndpointDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Register a new webhook endpoint", - "operationId": "create_3", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateWebhookEndpointRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookEndpointDto" - } - } - } - } - } - } - }, - "/api/v1/webhooks/{id}/test": { - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Send a test delivery to a webhook endpoint", - "operationId": "test", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestWebhookEndpointRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookTestResult" - } - } - } - } - } - } - }, - "/api/v1/webhooks/signing-secret/rotate": { - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Generate or rotate the organization webhook signing secret", - "operationId": "rotateSigningSecret", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseString" - } - } - } - } - } - } - }, - "/api/v1/vaults/rotate": { - "post": { - "tags": [ - "Vault" - ], - "summary": "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.", - "operationId": "rotateDek", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseDekRotationResultDto" - } - } - } - } - } - } - }, - "/api/v1/tags": { - "get": { - "tags": [ - "Tags" - ], - "summary": "List tags for the authenticated organization", - "operationId": "list_3", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Tags" - ], - "summary": "Create a new tag", - "operationId": "create_4", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTagRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTagDto" - } - } - } - } - } - } - }, - "/api/v1/service-subscriptions/{slug}": { - "post": { - "tags": [ - "Service Subscriptions" - ], - "summary": "Subscribe to a service or a component of a service", - "description": "Idempotent \u2014 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.", - "operationId": "subscribe", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServiceSubscribeRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" - } - } - } - } - } - } - }, - "/api/v1/secrets": { - "get": { - "tags": [ - "Secrets" - ], - "summary": "List secrets", - "operationId": "list_4", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultSecretDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Secrets" - ], - "summary": "Create secret", - "operationId": "create_5", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSecretRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseSecretDto" - } - } - } - } - } - } - }, - "/api/v1/resource-groups": { - "get": { - "tags": [ - "Resource Groups" - ], - "summary": "List all resource groups for the authenticated org with health summaries", - "operationId": "list_5", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultResourceGroupDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Resource Groups" - ], - "summary": "Create a new resource group", - "operationId": "create_6", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateResourceGroupRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupDto" - } - } - } - } - } - } - }, - "/api/v1/resource-groups/{id}/members": { - "post": { - "tags": [ - "Resource Groups" - ], - "summary": "Add a monitor or service member to a resource group", - "operationId": "addMember_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddResourceGroupMemberRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupMemberDto" - } - } - } - } - } - } - }, - "/api/v1/notification-policies": { - "get": { - "tags": [ - "Notification Policies" - ], - "summary": "List all notification policies for the authenticated org", - "operationId": "list_6", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationPolicyDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Notification Policies" - ], - "summary": "Create a notification policy with match rules and escalation chain", - "operationId": "create_7", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationPolicyRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationPolicyDto" - } - } - } - } - } - } - }, - "/api/v1/notification-policies/{id}/test": { - "post": { - "tags": [ - "Notification Policies" - ], - "summary": "Dry-run: evaluate a policy's match rules against a supplied incident context", - "operationId": "test_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestNotificationPolicyRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestMatchResult" - } - } - } - } - } - } - }, - "/api/v1/notification-dispatches/{id}/acknowledge": { - "post": { - "tags": [ - "Notification Dispatches" - ], - "summary": "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).", - "operationId": "acknowledge", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" - } - } - } - } - } - } - }, - "/api/v1/monitors": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "List monitors for the authenticated org", - "operationId": "list_7", - "parameters": [ - { - "name": "enabled", - "in": "query", - "description": "Filter by enabled state", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "required": false, - "schema": { - "type": "string", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] - } - }, - { - "name": "managedBy", - "in": "query", - "description": "Filter by managed-by source", - "required": false, - "schema": { - "type": "string", - "enum": [ - "DASHBOARD", - "CLI" - ] - } - }, - { - "name": "tags", - "in": "query", - "description": "Filter by tag names, comma-separated (e.g. prod,critical)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "search", - "in": "query", - "description": "Case-insensitive name search", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "environmentId", - "in": "query", - "description": "Filter by environment ID", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMonitorDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Monitors" - ], - "summary": "Create a new monitor", - "operationId": "create_8", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMonitorRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{monitorId}/assertions": { - "post": { - "tags": [ - "Monitor Assertions" - ], - "summary": "Add an assertion to a monitor", - "operationId": "add", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAssertionRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAssertionDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/test": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "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.", - "operationId": "testExisting", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/tags": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "Get all tags applied to a monitor", - "operationId": "getMonitorTags", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Monitors" - ], - "summary": "Add tags to a monitor; supports existing tag IDs and inline creation of new tags", - "operationId": "addMonitorTags", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddMonitorTagsRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultTagDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Monitors" - ], - "summary": "Remove tags from a monitor by their IDs", - "operationId": "removeMonitorTags", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RemoveMonitorTagsRequest" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/monitors/{id}/rotate-token": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "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.", - "operationId": "rotateToken", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/resume": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Resume a monitor (set enabled=true)", - "operationId": "resume", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/pause": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "Pause a monitor (set enabled=false)", - "operationId": "pause", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/test": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "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.", - "operationId": "testAdHoc", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MonitorTestRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorTestResultDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/bulk": { - "post": { - "tags": [ - "Monitors" - ], - "summary": "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.", - "operationId": "bulkAction", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkMonitorActionRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseBulkMonitorActionResult" - } - } - } - } - } - } - }, - "/api/v1/maintenance-windows": { - "get": { - "tags": [ - "Maintenance Windows" - ], - "summary": "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).", - "operationId": "list_8", - "parameters": [ - { - "name": "monitorId", - "in": "query", - "description": "Filter by monitor UUID", - "required": false, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "filter", - "in": "query", - "description": "Filter by status: 'active' or 'upcoming'", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMaintenanceWindowDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Maintenance Windows" - ], - "summary": "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.", - "operationId": "create_9", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMaintenanceWindowRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMaintenanceWindowDto" - } - } - } - } - } - } - }, - "/api/v1/invites": { - "get": { - "tags": [ - "Invites" - ], - "summary": "List invites", - "operationId": "list_9", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultInviteDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Invites" - ], - "summary": "Create invite", - "operationId": "create_10", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateInviteRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInviteDto" - } - } - } - } - } - } - }, - "/api/v1/invites/{inviteId}/revoke": { - "post": { - "tags": [ - "Invites" - ], - "summary": "Revoke invite", - "operationId": "revoke", - "parameters": [ - { - "name": "inviteId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/invites/{inviteId}/resend": { - "post": { - "tags": [ - "Invites" - ], - "summary": "Resend invite", - "operationId": "resend", - "parameters": [ - { - "name": "inviteId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseInviteDto" - } - } - } - } - } - } - }, - "/api/v1/incidents": { - "get": { - "tags": [ - "Incidents" - ], - "summary": "List incidents for the authenticated org", - "operationId": "list_10", - "parameters": [ - { - "name": "params", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/IncidentFilterParams" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultIncidentDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Incidents" - ], - "summary": "Create a manual incident", - "operationId": "create_11", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateManualIncidentRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" - } - } - } - } - } - } - }, - "/api/v1/incidents/{id}/updates": { - "post": { - "tags": [ - "Incidents" - ], - "summary": "Add an update to an incident (optionally change status)", - "operationId": "addUpdate", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AddIncidentUpdateRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" - } - } - } - } - } - } - }, - "/api/v1/incidents/{id}/resolve": { - "post": { - "tags": [ - "Incidents" - ], - "summary": "Resolve an incident", - "operationId": "resolve", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResolveIncidentRequest" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" - } - } - } - } - } - } - }, - "/api/v1/heartbeat/{token}": { - "get": { - "tags": [ - "Heartbeat" - ], - "summary": "Record a heartbeat ping (GET)", - "description": "Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.", - "operationId": "pingGet", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Ping endpoint token for the heartbeat monitor", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - } - } - } - } - } - }, - "post": { - "tags": [ - "Heartbeat" - ], - "summary": "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.", - "operationId": "pingPost", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Ping endpoint token for the heartbeat monitor", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "string" - } - }, - "text/plain": { - "schema": { - "type": "string" - } - }, - "*/*": { - "schema": { - "type": "string" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "boolean" - } - } - } - } - } - } - } - }, - "/api/v1/environments": { - "get": { - "tags": [ - "Environments" - ], - "summary": "List environments", - "operationId": "list_11", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultEnvironmentDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Environments" - ], - "summary": "Create environment", - "operationId": "create_12", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateEnvironmentRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseEnvironmentDto" - } - } - } - } - } - } - }, - "/api/v1/deploy/lock": { - "get": { - "tags": [ - "Deploy Lock" - ], - "summary": "Get current deploy lock", - "description": "Returns the active deploy lock for the current workspace, if any.", - "operationId": "current", - "parameters": [ - { - "name": "x-phelm-workspace-id", - "in": "header", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Deploy Lock" - ], - "summary": "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.", - "operationId": "acquire", - "parameters": [ - { - "name": "x-phelm-workspace-id", - "in": "header", - "description": "Target workspace ID (defaults to 1)", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AcquireDeployLockRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseDeployLockDto" - } - } - } - } - } - } - }, - "/api/v1/api-keys": { - "get": { - "tags": [ - "API Keys" - ], - "summary": "List API keys", - "operationId": "list_12", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultApiKeyDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "API Keys" - ], - "summary": "Create API key", - "operationId": "create_13", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateApiKeyRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" - } - } - } - } - } - } - }, - "/api/v1/api-keys/{id}/revoke": { - "post": { - "tags": [ - "API Keys" - ], - "summary": "Revoke API key", - "operationId": "revoke_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" - } - } - } - } - } - } - }, - "/api/v1/api-keys/{id}/regenerate": { - "post": { - "tags": [ - "API Keys" - ], - "summary": "Regenerate API key", - "operationId": "regenerate", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyCreateResponse" - } - } - } - } - } - } - }, - "/api/v1/alert-deliveries/{id}/retry": { - "post": { - "tags": [ - "Alert Deliveries" - ], - "summary": "Retry a failed delivery", - "description": "Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.", - "operationId": "retry", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAlertDeliveryDto" - } - } - } - } - } - } - }, - "/api/v1/alert-channels": { - "get": { - "tags": [ - "Alert Channels" - ], - "summary": "List active alert channels for the authenticated org", - "operationId": "list_13", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultAlertChannelDto" - } - } - } - } - } - }, - "post": { - "tags": [ - "Alert Channels" - ], - "summary": "Create a new alert channel with encrypted config", - "operationId": "create_14", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateAlertChannelRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Created", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAlertChannelDto" - } - } - } - } - } - } - }, - "/api/v1/alert-channels/{id}/test": { - "post": { - "tags": [ - "Alert Channels" - ], - "summary": "Test a saved alert channel's connectivity", - "operationId": "test_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" - } - } - } - } - } - } - }, - "/api/v1/alert-channels/test": { - "post": { - "tags": [ - "Alert Channels" - ], - "summary": "Test alert channel connectivity using raw config (no saved channel required)", - "operationId": "testConfig", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TestAlertChannelRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseTestChannelResult" - } - } - } - } - } - } - }, - "/v1/internal/service-incidents/by-ref/{serviceId}/{externalRef}/components": { - "patch": { - "tags": [ - "service-incident-internal-controller" - ], - "operationId": "addComponents", - "parameters": [ - { - "name": "serviceId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "externalRef", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ComponentUpdateRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultIncidentDto" - } - } - } - } - } - } - }, - "/api/v1/service-subscriptions/{id}/alert-sensitivity": { - "patch": { - "tags": [ - "Service Subscriptions" - ], - "summary": "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).", - "operationId": "updateAlertSensitivity", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateAlertSensitivityRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" - } - } - } - } - } - } - }, - "/api/v1/api-keys/{id}": { - "delete": { - "tags": [ - "API Keys" - ], - "summary": "Delete API key", - "operationId": "delete_10", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - }, - "patch": { - "tags": [ - "API Keys" - ], - "summary": "Update API key", - "operationId": "update_14", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateApiKeyRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseApiKeyDto" - } - } - } - } - } - } - }, - "/v1/internal/workspaces/{id}": { - "get": { - "tags": [ - "workspaces-controller" - ], - "operationId": "get_7", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWorkspaceDto" - } - } - } - } - } - } - }, - "/v1/internal/orgs/{id}/workspaces": { - "get": { - "tags": [ - "orgs-controller" - ], - "operationId": "listWorkspaces_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultWorkspaceDto" - } - } - } - } - } - } - }, - "/v1/internal/monitors/{id}/policy": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "policy", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentPolicyDto" - } - } - } - } - } - } - }, - "/v1/internal/monitors/{id}/env-variables": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "getEnvVariables", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMapStringString" - } - } - } - } - } - } - }, - "/v1/internal/monitors/{id}/auth": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "auth", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorAuthDto" - } - } - } - } - } - } - }, - "/v1/internal/monitors/{id}/assertions": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "getAssertions", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseListMonitorAssertionDto" - } - } - } - } - } - } - }, - "/v1/internal/monitors/{id}/active-incident": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "activeIncident", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDto" - } - } - } - } - } - } - }, - "/v1/internal/monitors/schedulable": { - "get": { - "tags": [ - "monitors-internal-controller" - ], - "operationId": "schedulable", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SchedulableMonitorDto" - } - } - } - } - } - } - } - }, - "/platform/plans": { - "get": { - "tags": [ - "Billing" - ], - "summary": "List public billing plans", - "operationId": "getPublicPlans", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseListBillingPlanDto" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}/subscriptions": { - "get": { - "tags": [ - "Subscriptions" - ], - "summary": "List active subscriptions", - "operationId": "listActive", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultSubscriptionDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Subscriptions" - ], - "operationId": "cancel", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/platform/orgs/{orgId}/subscriptions/upcoming-charge": { - "get": { - "tags": [ - "Subscriptions" - ], - "summary": "Get upcoming charge", - "operationId": "getUpcomingCharge", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "priceId", - "in": "query", - "required": true, - "schema": { - "minimum": 1, - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUpcomingChargeResponse" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}/subscriptions/management-urls": { - "get": { - "tags": [ - "Subscriptions" - ], - "summary": "Get subscription management URLs", - "operationId": "getManagementUrls", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMapStringString" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}/subscriptions/customer-auth-token": { - "get": { - "tags": [ - "Subscriptions" - ], - "summary": "Get customer auth token", - "operationId": "getCustomerAuthToken", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseString" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}/entitlements": { - "get": { - "tags": [ - "Entitlements" - ], - "summary": "Get resolved entitlements and current usage for the organization", - "operationId": "getEntitlements", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseEntitlementResponse" - } - } - } - } - } - } - }, - "/platform/orgs/search": { - "get": { - "tags": [ - "Organizations" - ], - "summary": "Search organizations", - "operationId": "searchOrganizations", - "parameters": [ - { - "name": "query", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "paginationParams", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/PaginationParams" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultIdValuePair" - } - } - } - } - } - } - }, - "/platform/me/orgs": { - "get": { - "tags": [ - "Me" - ], - "summary": "Get current user's organizations", - "operationId": "myOrgs", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMyOrgItemDto" - } - } - } - } - } - } - }, - "/platform/events/stream": { - "get": { - "tags": [ - "Events" - ], - "summary": "Subscribe to real-time platform events via SSE", - "operationId": "stream", - "responses": { - "200": { - "description": "OK", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/SseEmitter" - } - } - } - } - } - } - }, - "/platform/admin/users": { - "get": { - "tags": [ - "admin-controller" - ], - "operationId": "listUsers", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultUserDto" - } - } - } - } - } - } - }, - "/platform/admin/stats": { - "get": { - "tags": [ - "admin-controller" - ], - "operationId": "getStats", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAdminStatsDto" - } - } - } - } - } - } - }, - "/platform/admin/orgs": { - "get": { - "tags": [ - "admin-controller" - ], - "operationId": "listOrgs", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultOrganizationDto" - } - } - } - } - } - } - }, - "/platform/admin/adapters/health": { - "get": { - "tags": [ - "admin-adapter-health-controller" - ], - "operationId": "getAdapterHealth", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultAdapterHealthDto" - } - } - } - } - } - } - }, - "/api/v1/webhooks/{id}/deliveries": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List recent deliveries for a webhook endpoint", - "operationId": "listDeliveries", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultWebhookDeliveryDto" - } - } - } - } - } - } - }, - "/api/v1/webhooks/signing-secret": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "Get signing secret metadata for the authenticated org", - "operationId": "getSigningSecretInfo", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseWebhookSigningSecretDto" - } - } - } - } - } - } - }, - "/api/v1/webhooks/events": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "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.", - "operationId": "listEvents", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/WebhookEventCatalogResponse" - } - } - } - } - } - } - }, - "/api/v1/services": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List all enabled services (cursor-paginated)", - "operationId": "listServices", - "parameters": [ - { - "name": "category", - "in": "query", - "description": "Filter by category (exact match)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter by current overall_status (exact match)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "cursor", - "in": "query", - "description": "Opaque cursor from a previous response", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Page size (1\u2013100, default 20)", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CursorPageServiceCatalogDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get a single service by slug or UUID with current status, components, and recent incidents", - "operationId": "getService", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceDetailDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}/uptime": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get uptime statistics for a service", - "description": "Uptime data aggregated across active non-group components.", - "operationId": "getServiceUptime", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "period", - "in": "query", - "description": "Time window", - "required": false, - "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d", - "1y", - "2y", - "all" - ] - } - }, - { - "name": "granularity", - "in": "query", - "description": "Bucket granularity", - "required": false, - "schema": { - "type": "string", - "enum": [ - "hourly", - "daily", - "monthly" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceUptimeResponse" - } - } - } - } - }, - "security": [ - { - "BearerAuth": [] - } - ] - } - }, - "/api/v1/services/{slugOrId}/maintenances": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List scheduled maintenances for a service", - "operationId": "getScheduledMaintenances", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter by status (e.g. scheduled, in_progress, verifying, completed)", - "required": false, - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultScheduledMaintenanceDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}/incidents": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List incident history for a service (paginated)", - "operationId": "listIncidents", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "from", - "in": "query", - "description": "Earliest start date (ISO 8601 date)", - "required": false, - "schema": { - "type": "string", - "format": "date" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter: active (unresolved), resolved, or omit for all", - "required": false, - "schema": { - "type": "string", - "enum": [ - "active", - "resolved" - ] - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}/incidents/{incidentId}": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get incident detail with full update timeline", - "operationId": "getIncident", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "incidentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceIncidentDetailDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}/components": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List active components for a service with current status and inline uptime", - "operationId": "getComponents", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultServiceComponentDto" - } - } - } - } - } - } - }, - "/api/v1/services/{slugOrId}/components/{componentId}/uptime": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Get daily uptime data for a component", - "operationId": "getComponentUptime", - "parameters": [ - { - "name": "slugOrId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "componentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "period", - "in": "query", - "description": "Time window", - "required": false, - "schema": { - "type": "string", - "enum": [ - "7d", - "30d", - "90d", - "1y" - ] - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultComponentUptimeDayDto" - } - } - } - } - } - } - }, - "/api/v1/services/summary": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "Global status summary across all services", - "description": "Returns aggregate counts of services by status and a list of services currently experiencing issues.", - "operationId": "getGlobalStatusSummary", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseGlobalStatusSummaryDto" - } - } - } - } - } - } - }, - "/api/v1/services/incidents": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List vendor incidents across all services (paginated)", - "description": "Cross-service vendor incident feed ordered by start date descending.", - "operationId": "listCrossServiceIncidents", - "parameters": [ - { - "name": "from", - "in": "query", - "description": "Earliest start date (ISO 8601 date)", - "required": false, - "schema": { - "type": "string", - "format": "date" - } - }, - { - "name": "status", - "in": "query", - "description": "Filter: active (unresolved), resolved, or omit for all", - "required": false, - "schema": { - "type": "string", - "enum": [ - "active", - "resolved" - ] - } - }, - { - "name": "category", - "in": "query", - "description": "Filter by service category", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultServiceIncidentDto" - } - } - } - } - } - } - }, - "/api/v1/service-subscriptions": { - "get": { - "tags": [ - "Service Subscriptions" - ], - "summary": "List all service subscriptions for the organization", - "operationId": "list_14", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultServiceSubscriptionDto" - } - } - } - } - } - } - }, - "/api/v1/service-subscriptions/{id}": { - "get": { - "tags": [ - "Service Subscriptions" - ], - "summary": "Get a subscription by its ID", - "operationId": "get_8", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseServiceSubscriptionDto" - } - } - } - } - } - }, - "delete": { - "tags": [ - "Service Subscriptions" - ], - "summary": "Remove a subscription by its ID", - "description": "Removes a specific subscription (whole-service or component-level). No-op if not found.", - "operationId": "unsubscribe", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/resource-groups/{id}/health": { - "get": { - "tags": [ - "Resource Groups" - ], - "summary": "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.", - "operationId": "getHealth", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResourceGroupHealthDto" - } - } - } - } - } - } - }, - "/api/v1/notifications": { - "get": { - "tags": [ - "Notifications" - ], - "summary": "List notifications for the current user", - "operationId": "list_15", - "parameters": [ - { - "name": "unreadOnly", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": false - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 0 - } - }, - { - "name": "size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 20 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDto" - } - } - } - } - } - } - }, - "/api/v1/notifications/unread-count": { - "get": { - "tags": [ - "Notifications" - ], - "summary": "Get unread notification count", - "operationId": "unreadCount", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseLong" - } - } - } - } - } - } - }, - "/api/v1/notification-policies/{id}/dispatches": { - "get": { - "tags": [ - "Notification Policies" - ], - "summary": "List all dispatches (firing history) for a notification policy", - "operationId": "listDispatches", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" - } - } - } - } - } - } - }, - "/api/v1/notification-dispatches": { - "get": { - "tags": [ - "Notification Dispatches" - ], - "summary": "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.", - "operationId": "listByIncident", - "parameters": [ - { - "name": "incident_id", - "in": "query", - "description": "UUID of the incident to inspect", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultNotificationDispatchDto" - } - } - } - } - } - } - }, - "/api/v1/notification-dispatches/{id}": { - "get": { - "tags": [ - "Notification Dispatches" - ], - "summary": "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.", - "operationId": "getById_2", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseNotificationDispatchDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/versions": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "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.", - "operationId": "listVersions", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMonitorVersionDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/versions/{version}": { - "get": { - "tags": [ - "Monitors" - ], - "summary": "Get a specific version snapshot for a monitor", - "description": "Returns the full monitor config snapshot captured at the given version number.", - "operationId": "getVersion", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "version", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseMonitorVersionDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/uptime": { - "get": { - "tags": [ - "Check Results" - ], - "summary": "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.", - "operationId": "getUptime", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "window", - "in": "query", - "description": "Time window for uptime calculation", - "required": false, - "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d" - ] - } - } - ], - "responses": { - "200": { - "description": "Uptime statistics", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/UptimeDto" - } - } - } - }, - "400": { - "description": "Invalid window parameter", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" - } - } - } - }, - "403": { - "description": "Monitor does not belong to the caller's org", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" - } - } - } - }, - "404": { - "description": "Monitor not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseUptimeDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/results": { - "get": { - "tags": [ - "Check Results" - ], - "summary": "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 \u2014 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.", - "operationId": "getResults", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "from", - "in": "query", - "description": "Start of time range (ISO 8601, inclusive); defaults to 24 hours ago", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "to", - "in": "query", - "description": "End of time range (ISO 8601, inclusive); defaults to now", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "cursor", - "in": "query", - "description": "Opaque cursor from a previous response for pagination", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "description": "Maximum results per page (1\u2013200)", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 50 - }, - "example": 50 - }, - { - "name": "region", - "in": "query", - "description": "Filter by region (e.g. us-east)", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "passed", - "in": "query", - "description": "Filter by pass/fail status", - "required": false, - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "Paginated check results", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CursorPage" - } - } - } - }, - "400": { - "description": "Invalid query parameters", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" - } - } - } - }, - "403": { - "description": "Monitor does not belong to the caller's org", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" - } - } - } - }, - "404": { - "description": "Monitor not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/CursorPageCheckResultDto" - } - } - } - } - } - } - }, - "/api/v1/monitors/{id}/results/summary": { - "get": { - "tags": [ - "Check Results" - ], - "summary": "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.", - "operationId": "getSummary", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "chartWindow", - "in": "query", - "description": "Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets", - "required": false, - "schema": { - "type": "string", - "enum": [ - "24h", - "7d", - "30d", - "90d" - ] - } - } - ], - "responses": { - "200": { - "description": "Results summary", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/ResultSummaryDto" - } - } - } - }, - "400": { - "description": "Invalid chartWindow parameter", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" - } - } - } - }, - "403": { - "description": "Monitor does not belong to the caller's org", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" - } - } - } - }, - "404": { - "description": "Monitor not found", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseResultSummaryDto" - } - } - } - } - } - } - }, - "/api/v1/members": { - "get": { - "tags": [ - "Members" - ], - "summary": "List organization members", - "operationId": "list_16", - "parameters": [ - { - "name": "pageable", - "in": "query", - "required": true, - "schema": { - "$ref": "#/components/schemas/Pageable" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultMemberDto" - } - } - } - } - } - } - }, - "/api/v1/integrations": { - "get": { - "tags": [ - "Integrations" - ], - "summary": "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.", - "operationId": "list_17", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/IntegrationCatalogResponse" - } - } - } - } - } - } - }, - "/api/v1/incidents/{id}": { - "get": { - "tags": [ - "Incidents" - ], - "summary": "Get incident details including update timeline", - "operationId": "get_9", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseIncidentDetailDto" - } - } - } - } - } - } - }, - "/api/v1/dashboard/overview": { - "get": { - "tags": [ - "Dashboard" - ], - "summary": "Dashboard overview", - "description": "Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.", - "operationId": "overview", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseDashboardOverviewDto" - } - } - } - } - } - } - }, - "/api/v1/categories": { - "get": { - "tags": [ - "Status Data" - ], - "summary": "List categories with service counts", - "operationId": "listCategories", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultCategoryDto" - } - } - } - } - } - } - }, - "/api/v1/auth/me": { - "get": { - "tags": [ - "API Auth" - ], - "summary": "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_...).", - "operationId": "me_1", - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/SingleValueResponseAuthMeResponse" - } - } - } - } - } - } - }, - "/api/v1/audit-log": { - "get": { - "tags": [ - "Audit Log" - ], - "summary": "List audit events for the current organization", - "operationId": "list_18", - "parameters": [ - { - "name": "action", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "actorId", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "resourceType", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "from", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "to", - "in": "query", - "required": false, - "schema": { - "type": "string", - "format": "date-time" - } - }, - { - "name": "page", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 0 - } - }, - { - "name": "size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "format": "int32", - "default": 50 - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/PageResultAuditEventDto" - } - } - } - } - } - } - }, - "/api/v1/alert-deliveries/{id}/attempts": { - "get": { - "tags": [ - "Alert Deliveries" - ], - "summary": "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.", - "operationId": "listAttempts", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultDeliveryAttemptDto" - } - } - } - } - } - } - }, - "/api/v1/alert-channels/{id}/deliveries": { - "get": { - "tags": [ - "Alert Channels" - ], - "summary": "List delivery history for an alert channel", - "operationId": "listDeliveries_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "*/*": { - "schema": { - "$ref": "#/components/schemas/TableValueResultAlertDeliveryDto" - } - } - } - } - } - } - }, - "/platform/orgs/{orgId}": { - "delete": { - "tags": [ - "Organizations" - ], - "summary": "Delete organization", - "operationId": "delete_11", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/platform/admin/orgs/{orgId}/members/{userId}": { - "delete": { - "tags": [ - "admin-member-controller" - ], - "operationId": "removeMember", - "parameters": [ - { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/resource-groups/{id}/members/{memberId}": { - "delete": { - "tags": [ - "Resource Groups" - ], - "summary": "Remove a member from a resource group", - "operationId": "removeMember_1", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "memberId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/members/{userId}": { - "delete": { - "tags": [ - "Members" - ], - "summary": "Remove member from organization", - "operationId": "remove_2", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/deploy/lock/{lockId}": { - "delete": { - "tags": [ - "Deploy Lock" - ], - "summary": "Release deploy lock", - "description": "Releases a deploy lock by ID. Only the lock holder should call this.", - "operationId": "release", - "parameters": [ - { - "name": "lockId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "x-phelm-workspace-id", - "in": "header", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - }, - "/api/v1/deploy/lock/force": { - "delete": { - "tags": [ - "Deploy Lock" - ], - "summary": "Force-release deploy lock", - "description": "Forcibly removes any deploy lock on the current workspace. Use to break stale locks.", - "operationId": "forceRelease", - "parameters": [ - { - "name": "x-phelm-workspace-id", - "in": "header", - "required": false, - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "204": { - "description": "No Content" - } - } - } - } - }, - "components": { - "schemas": { - "CreateSubscriptionRequest": { - "type": "object", - "properties": { - "priceId": { - "minimum": 1, - "type": "integer", - "format": "int32" - } - } - }, - "BillingPlanDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique billing plan identifier", - "format": "int32" - }, - "paddleId": { - "type": "string", - "description": "Paddle product identifier" - }, - "name": { - "type": "string", - "description": "Billing plan display name" - }, - "description": { - "type": "string", - "description": "Plan description", - "nullable": true - }, - "prices": { - "type": "array", - "description": "Available prices for this plan; null when not requested", - "nullable": true, - "items": { - "$ref": "#/components/schemas/BillingPriceDto" - } - } - }, - "description": "Associated billing plan; null when not requested", - "nullable": true - }, - "BillingPriceDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique billing price identifier", - "format": "int32" - }, - "paddleId": { - "type": "string", - "description": "Paddle price identifier" - }, - "amount": { - "type": "integer", - "description": "Price amount in smallest currency unit (e.g. cents)", - "format": "int32" - }, - "interval": { - "type": "string", - "description": "Billing interval (MONTH or YEAR)", - "enum": [ - "DAY", - "WEEK", - "MONTH", - "YEAR" - ] - }, - "intervalCount": { - "type": "integer", - "description": "Number of intervals between billing cycles", - "format": "int32" - }, - "description": { - "type": "string", - "description": "Price description", - "nullable": true - }, - "billingPlan": { - "$ref": "#/components/schemas/BillingPlanDto" - } - }, - "description": "Price details for this line item" - }, - "ItemDto": { - "type": "object", - "properties": { - "billingPrice": { - "$ref": "#/components/schemas/BillingPriceDto" - }, - "quantity": { - "type": "integer", - "description": "Quantity of this price", - "format": "int32" - }, - "amount": { - "type": "integer", - "description": "Line item total in smallest currency unit", - "format": "int32" - } - }, - "description": "Line items included in this subscription" - }, - "SingleValueResponseSubscriptionDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SubscriptionDto" - } - } - }, - "SubscriptionDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Internal subscription identifier", - "format": "int32" - }, - "paddleId": { - "type": "string", - "description": "Paddle subscription identifier" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the subscription was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the subscription was last updated", - "format": "date-time" - }, - "organizationId": { - "type": "integer", - "description": "Organization this subscription belongs to", - "format": "int32" - }, - "status": { - "type": "string", - "description": "Current subscription status", - "enum": [ - "ACTIVE", - "CANCELED", - "PAST_DUE", - "PAUSED", - "TRIALING" - ] - }, - "nextBilledAt": { - "type": "string", - "description": "Next billing date; null when cancelled or expired", - "format": "date-time", - "nullable": true - }, - "willCancelAt": { - "type": "string", - "description": "Scheduled cancellation date; null if no cancellation pending", - "format": "date-time", - "nullable": true - }, - "items": { - "type": "array", - "description": "Line items included in this subscription", - "items": { - "$ref": "#/components/schemas/ItemDto" - } - } - }, - "description": "Current billing subscription details" - }, - "UpdateOrgDetailsRequest": { - "required": [ - "email", - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "New organization name (max 200 chars)" - }, - "email": { - "minLength": 1, - "type": "string", - "description": "New billing and contact email address", - "format": "email" - }, - "size": { - "maxLength": 50, - "minLength": 0, - "type": "string", - "description": "Team size range (e.g. 1-10, 11-50)" - }, - "industry": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "Industry vertical (e.g. SaaS, Fintech)" - }, - "websiteUrl": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Organization website URL (max 255 chars)" - } - } - }, - "OrganizationDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique organization identifier", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Organization name" - }, - "email": { - "type": "string", - "description": "Billing and contact email", - "nullable": true - }, - "size": { - "type": "string", - "description": "Team size range (e.g. 1-10, 11-50)", - "nullable": true - }, - "industry": { - "type": "string", - "description": "Industry vertical (e.g. SaaS, Fintech)", - "nullable": true - }, - "websiteUrl": { - "type": "string", - "description": "Organization website URL", - "nullable": true - } - }, - "description": "Organization account details" - }, - "SingleValueResponseOrganizationDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/OrganizationDto" - } - } - }, - "UpdateOnboardingStageRequest": { - "required": [ - "stage" - ], - "type": "object", - "properties": { - "stage": { - "type": "string", - "description": "New onboarding stage", - "enum": [ - "WELCOME", - "FIRST_MONITOR", - "SETUP_COMPLETE", - "COMPLETED" - ] - } - }, - "description": "Advance the user's onboarding stage" - }, - "SingleValueResponseUserDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/UserDto" - } - } - }, - "UserDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique user identifier", - "format": "int32" - }, - "email": { - "type": "string", - "description": "User email address" - }, - "emailVerified": { - "type": "boolean", - "description": "Whether the email address has been verified" - }, - "name": { - "type": "string", - "description": "Display name; null if not set", - "nullable": true - }, - "userRole": { - "type": "string", - "description": "Platform role: USER or SUPERADMIN", - "enum": [ - "SUPERADMIN", - "ADMIN", - "USER" - ] - }, - "onboardingStage": { - "type": "string", - "description": "Current onboarding progress stage; null when completed", - "nullable": true, - "enum": [ - "WELCOME", - "FIRST_MONITOR", - "SETUP_COMPLETE", - "COMPLETED" - ] - }, - "imageUrl": { - "type": "string", - "description": "Profile image URL; null if not set", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the account was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the account was last updated", - "format": "date-time" - } - }, - "description": "User account details" - }, - "UpdateProfileRequest": { - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "New display name (max 200 chars)" - } - } - }, - "UpdateNotificationPreferencesRequest": { - "required": [ - "preferences" - ], - "type": "object", - "properties": { - "preferences": { - "type": "object", - "additionalProperties": { - "type": "boolean", - "description": "Map of category keys to enabled/disabled flags" - }, - "description": "Map of category keys to enabled/disabled flags" - } - }, - "description": "Replace notification preferences for the current user" - }, - "NotificationPreferencesDto": { - "type": "object", - "properties": { - "preferences": { - "type": "object", - "additionalProperties": { - "type": "boolean", - "description": "Map of category keys to enabled/disabled flags" - }, - "description": "Map of category keys to enabled/disabled flags" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when preferences were last updated", - "format": "date-time" - } - }, - "description": "User notification preferences keyed by notification category" - }, - "SingleValueResponseNotificationPreferencesDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/NotificationPreferencesDto" - } - } - }, - "UpdateWorkspaceRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "New workspace name" - } - }, - "description": "Update workspace details" - }, - "SingleValueResponseWorkspaceDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/WorkspaceDto" - } - } - }, - "WorkspaceDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique workspace identifier", - "format": "int32" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the workspace was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the workspace was last updated", - "format": "date-time" - }, - "name": { - "type": "string", - "description": "Workspace name" - }, - "orgId": { - "type": "integer", - "description": "Organization this workspace belongs to", - "format": "int32" - } - }, - "description": "Workspace within an organization" - }, - "UpdateUserRequest": { - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "New display name (max 200 chars)" - }, - "email": { - "type": "string", - "description": "New email address", - "format": "email" - }, - "userRole": { - "type": "string", - "description": "New platform role", - "enum": [ - "SUPERADMIN", - "ADMIN", - "USER" - ] - }, - "onboardingStage": { - "type": "string", - "description": "New onboarding stage", - "enum": [ - "WELCOME", - "FIRST_MONITOR", - "SETUP_COMPLETE", - "COMPLETED" - ] - }, - "imageUrl": { - "maxLength": 500, - "minLength": 0, - "type": "string", - "description": "New profile image URL (max 500 chars)" - } - } - }, - "ChangeRoleRequest": { - "required": [ - "orgRole" - ], - "type": "object", - "properties": { - "orgRole": { - "type": "string", - "description": "New role to assign", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - } - }, - "description": "Update an organization member's role" - }, - "UpdateWebhookEndpointRequest": { - "type": "object", - "properties": { - "url": { - "maxLength": 2048, - "minLength": 0, - "type": "string", - "description": "New webhook URL; null preserves current", - "nullable": true - }, - "description": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "New description; null preserves current", - "nullable": true - }, - "subscribedEvents": { - "type": "array", - "description": "Replace subscribed events; null preserves current", - "nullable": true, - "items": { - "type": "string", - "description": "Replace subscribed events; null preserves current", - "nullable": true - } - }, - "enabled": { - "type": "boolean", - "description": "Enable or disable delivery; null preserves current", - "nullable": true - } - } - }, - "SingleValueResponseWebhookEndpointDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/WebhookEndpointDto" - } - } - }, - "WebhookEndpointDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique webhook endpoint identifier", - "format": "uuid" - }, - "url": { - "type": "string", - "description": "HTTPS endpoint URL that receives event payloads" - }, - "description": { - "type": "string", - "description": "Human-readable description of this endpoint", - "nullable": true - }, - "subscribedEvents": { - "type": "array", - "description": "Event types this endpoint is subscribed to", - "items": { - "type": "string", - "description": "Event types this endpoint is subscribed to" - } - }, - "enabled": { - "type": "boolean", - "description": "Whether delivery is enabled for this endpoint" - }, - "consecutiveFailures": { - "type": "integer", - "description": "Number of consecutive delivery failures", - "format": "int32" - }, - "disabledReason": { - "type": "string", - "description": "Reason the endpoint was auto-disabled", - "nullable": true - }, - "disabledAt": { - "type": "string", - "description": "Timestamp when the endpoint was auto-disabled", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the endpoint was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the endpoint was last updated", - "format": "date-time" - } - }, - "description": "Webhook endpoint that receives event delivery payloads" - }, - "UpdateTagRequest": { - "type": "object", - "properties": { - "name": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "New tag name", - "nullable": true - }, - "color": { - "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", - "type": "string", - "description": "New hex color code", - "nullable": true - } - }, - "description": "Request body for updating a tag; null fields are left unchanged" - }, - "SingleValueResponseTagDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/TagDto" - } - } - }, - "TagDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique tag identifier", - "format": "uuid" - }, - "organizationId": { - "type": "integer", - "description": "Organization this tag belongs to", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Tag name, unique within the org" - }, - "color": { - "type": "string", - "description": "Hex color code for display (e.g. #6B7280)" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the tag was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the tag was last updated", - "format": "date-time" - } - }, - "description": "Tag for organizing and filtering monitors" - }, - "UpdateSecretRequest": { - "required": [ - "value" - ], - "type": "object", - "properties": { - "value": { - "maxLength": 32768, - "minLength": 0, - "type": "string", - "description": "New secret value, stored encrypted (max 32KB)" - } - } - }, - "MonitorReference": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Monitor identifier", - "format": "uuid" - }, - "name": { - "type": "string", - "description": "Monitor name" - } - }, - "description": "Monitors that reference this secret; null on create/update responses" - }, - "SecretDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique secret identifier", - "format": "uuid" - }, - "key": { - "type": "string", - "description": "Secret key name, unique within the workspace" - }, - "dekVersion": { - "type": "integer", - "description": "DEK version at the time of last encryption", - "format": "int32" - }, - "valueHash": { - "type": "string", - "description": "SHA-256 hex digest of the current plaintext; use for change detection" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the secret was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the secret was last updated", - "format": "date-time" - }, - "usedByMonitors": { - "type": "array", - "description": "Monitors that reference this secret; null on create/update responses", - "nullable": true, - "items": { - "$ref": "#/components/schemas/MonitorReference" - } - } - }, - "description": "Secret with change-detection hash; plaintext value is never returned" - }, - "SingleValueResponseSecretDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SecretDto" - } - } - }, - "RetryStrategy": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Retry strategy kind, e.g. fixed interval between attempts" - }, - "maxRetries": { - "type": "integer", - "description": "Maximum number of retries after a failed check", - "format": "int32" - }, - "interval": { - "type": "integer", - "description": "Delay between retry attempts in seconds", - "format": "int32" - } - }, - "description": "Default retry strategy for member monitors; null clears" - }, - "UpdateResourceGroupRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this group" - }, - "description": { - "type": "string", - "description": "Optional description; null clears the existing value", - "nullable": true - }, - "alertPolicyId": { - "type": "string", - "description": "Optional notification policy to apply for this group; null clears the existing value", - "format": "uuid", - "nullable": true - }, - "defaultFrequency": { - "maximum": 86400, - "minimum": 30, - "type": "integer", - "description": "Default check frequency in seconds for members (30\u201386400); null clears", - "format": "int32", - "nullable": true - }, - "defaultRegions": { - "type": "array", - "description": "Default regions for member monitors; null clears", - "nullable": true, - "items": { - "type": "string", - "description": "Default regions for member monitors; null clears", - "nullable": true - } - }, - "defaultRetryStrategy": { - "$ref": "#/components/schemas/RetryStrategy" - }, - "defaultAlertChannels": { - "type": "array", - "description": "Default alert channel IDs for member monitors; null clears", - "nullable": true, - "items": { - "type": "string", - "description": "Default alert channel IDs for member monitors; null clears", - "format": "uuid", - "nullable": true - } - }, - "defaultEnvironmentId": { - "type": "string", - "description": "Default environment ID for member monitors; null clears", - "format": "uuid", - "nullable": true - }, - "healthThresholdType": { - "type": "string", - "description": "Health threshold type: COUNT or PERCENTAGE; null disables threshold", - "nullable": true, - "enum": [ - "COUNT", - "PERCENTAGE" - ] - }, - "healthThresholdValue": { - "maximum": 100, - "exclusiveMaximum": false, - "minimum": 0, - "exclusiveMinimum": false, - "type": "number", - "description": "Health threshold value; null disables threshold", - "nullable": true - }, - "suppressMemberAlerts": { - "type": "boolean", - "description": "Suppress member-level alert notifications; null preserves current value", - "nullable": true - }, - "confirmationDelaySeconds": { - "maximum": 600, - "minimum": 0, - "type": "integer", - "description": "Confirmation delay in seconds; null clears", - "format": "int32", - "nullable": true - }, - "recoveryCooldownMinutes": { - "maximum": 60, - "minimum": 0, - "type": "integer", - "description": "Recovery cooldown in minutes; null clears", - "format": "int32", - "nullable": true - } - }, - "description": "Request body for updating a resource group" - }, - "ResourceGroupDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique resource group identifier", - "format": "uuid" - }, - "organizationId": { - "type": "integer", - "description": "Organization this group belongs to", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable group name" - }, - "slug": { - "type": "string", - "description": "URL-safe group identifier" - }, - "description": { - "type": "string", - "description": "Optional group description", - "nullable": true - }, - "alertPolicyId": { - "type": "string", - "description": "Notification policy applied to this group", - "format": "uuid", - "nullable": true - }, - "defaultFrequency": { - "type": "integer", - "description": "Default check frequency in seconds for member monitors", - "format": "int32", - "nullable": true - }, - "defaultRegions": { - "type": "array", - "description": "Default regions for member monitors", - "nullable": true, - "items": { - "type": "string", - "description": "Default regions for member monitors", - "nullable": true - } - }, - "defaultRetryStrategy": { - "$ref": "#/components/schemas/RetryStrategy" - }, - "defaultAlertChannels": { - "type": "array", - "description": "Default alert channel IDs for member monitors", - "nullable": true, - "items": { - "type": "string", - "description": "Default alert channel IDs for member monitors", - "format": "uuid", - "nullable": true - } - }, - "defaultEnvironmentId": { - "type": "string", - "description": "Default environment ID for member monitors", - "format": "uuid", - "nullable": true - }, - "healthThresholdType": { - "type": "string", - "description": "Health threshold type: COUNT or PERCENTAGE", - "nullable": true, - "enum": [ - "COUNT", - "PERCENTAGE" - ] - }, - "healthThresholdValue": { - "type": "number", - "description": "Health threshold value", - "nullable": true - }, - "suppressMemberAlerts": { - "type": "boolean", - "description": "When true, member-level incidents skip notification dispatch; only group alerts fire" - }, - "confirmationDelaySeconds": { - "type": "integer", - "description": "Seconds to wait after health threshold breach before creating group incident", - "format": "int32", - "nullable": true - }, - "recoveryCooldownMinutes": { - "type": "integer", - "description": "Cooldown minutes after group incident resolves before a new one can open", - "format": "int32", - "nullable": true - }, - "health": { - "$ref": "#/components/schemas/ResourceGroupHealthDto" - }, - "members": { - "type": "array", - "description": "Member list with individual statuses; populated on detail GET only", - "nullable": true, - "items": { - "$ref": "#/components/schemas/ResourceGroupMemberDto" - } - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the group was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the group was last updated", - "format": "date-time" - } - }, - "description": "Resource group with health summary and optional member details" - }, - "ResourceGroupHealthDto": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Worst-of health status across all members", - "enum": [ - "operational", - "maintenance", - "degraded", - "down" - ] - }, - "totalMembers": { - "type": "integer", - "description": "Total number of members in the group", - "format": "int32" - }, - "operationalCount": { - "type": "integer", - "description": "Number of members currently in operational status", - "format": "int32" - }, - "activeIncidents": { - "type": "integer", - "description": "Number of members with an active incident or non-operational status", - "format": "int32" - }, - "thresholdStatus": { - "type": "string", - "description": "Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured.", - "nullable": true, - "enum": [ - "healthy", - "degraded", - "down" - ] - }, - "failingCount": { - "type": "integer", - "description": "Number of failing members at time of last evaluation", - "format": "int32", - "nullable": true - } - }, - "description": "Aggregated health summary for a resource group" - }, - "ResourceGroupMemberDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique group member record identifier", - "format": "uuid" - }, - "groupId": { - "type": "string", - "description": "Resource group this member belongs to", - "format": "uuid" - }, - "memberType": { - "type": "string", - "description": "Type of member: 'monitor' or 'service'" - }, - "monitorId": { - "type": "string", - "description": "Monitor ID; set when memberType is 'monitor'", - "format": "uuid", - "nullable": true - }, - "serviceId": { - "type": "string", - "description": "Service ID; set when memberType is 'service'", - "format": "uuid", - "nullable": true - }, - "name": { - "type": "string", - "description": "Display name of the referenced monitor or service", - "nullable": true - }, - "slug": { - "type": "string", - "description": "Slug identifier for the service (services only); used for icons and uptime API calls", - "nullable": true - }, - "subscriptionId": { - "type": "string", - "description": "Subscription ID for the service (services only); used to link to the dependency detail page", - "format": "uuid", - "nullable": true - }, - "status": { - "type": "string", - "description": "Computed health status for this member", - "enum": [ - "operational", - "maintenance", - "degraded", - "down" - ] - }, - "effectiveFrequency": { - "type": "string", - "description": "Effective check frequency label showing the group default when the monitor inherits it; null for services or when no group default is configured", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the member was added to the group", - "format": "date-time" - }, - "uptime24h": { - "type": "number", - "description": "24h uptime percentage; populated when includeMetrics=true", - "format": "double", - "nullable": true - }, - "chartData": { - "type": "array", - "description": "Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true", - "nullable": true, - "items": { - "type": "number", - "description": "Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true", - "format": "double", - "nullable": true - } - }, - "avgLatencyMs": { - "type": "number", - "description": "Average latency in ms (monitors only); populated when includeMetrics=true", - "format": "double", - "nullable": true - }, - "p95LatencyMs": { - "type": "number", - "description": "P95 latency in ms (monitors only); populated when includeMetrics=true", - "format": "double", - "nullable": true - }, - "lastCheckedAt": { - "type": "string", - "description": "Timestamp of the most recent health check; populated when includeMetrics=true", - "format": "date-time", - "nullable": true - }, - "monitorType": { - "type": "string", - "description": "Monitor type (HTTP, DNS, TCP, ICMP, HEARTBEAT, MCP); monitors only", - "nullable": true - }, - "environmentName": { - "type": "string", - "description": "Environment name; monitors only", - "nullable": true - } - }, - "description": "A single member of a resource group with its computed health status" - }, - "SingleValueResponseResourceGroupDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ResourceGroupDto" - } - } - }, - "EscalationChain": { - "required": [ - "steps" - ], - "type": "object", - "properties": { - "steps": { - "minItems": 1, - "type": "array", - "description": "Ordered escalation steps, evaluated in sequence", - "items": { - "$ref": "#/components/schemas/EscalationStep" - } - }, - "onResolve": { - "type": "string", - "description": "Action when the incident resolves", - "nullable": true - }, - "onReopen": { - "type": "string", - "description": "Action when a resolved incident reopens", - "nullable": true - } - }, - "description": "Escalation chain defining which channels to notify" - }, - "EscalationStep": { - "required": [ - "channelIds" - ], - "type": "object", - "properties": { - "delayMinutes": { - "minimum": 0, - "type": "integer", - "description": "Minutes to wait before executing this step (0 = immediate)", - "format": "int32" - }, - "channelIds": { - "minItems": 1, - "type": "array", - "description": "Alert channel IDs to notify in this step", - "items": { - "type": "string", - "description": "Alert channel IDs to notify in this step", - "format": "uuid" - } - }, - "requireAck": { - "type": "boolean", - "description": "Whether an acknowledgment is required before escalating", - "nullable": true - }, - "repeatIntervalSeconds": { - "minimum": 1, - "type": "integer", - "description": "Repeat notification interval in seconds until acknowledged", - "format": "int32", - "nullable": true - } - }, - "description": "Ordered escalation steps, evaluated in sequence" - }, - "MatchRule": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Rule type, e.g. severity_gte, monitor_id_in, region_in" - }, - "value": { - "type": "string", - "description": "Comparison value for single-value rules like severity_gte", - "nullable": true - }, - "monitorIds": { - "type": "array", - "description": "Monitor UUIDs to match for monitor_id_in rules", - "nullable": true, - "items": { - "type": "string", - "description": "Monitor UUIDs to match for monitor_id_in rules", - "format": "uuid", - "nullable": true - } - }, - "regions": { - "type": "array", - "description": "Region codes to match for region_in rules", - "nullable": true, - "items": { - "type": "string", - "description": "Region codes to match for region_in rules", - "nullable": true - } - }, - "values": { - "type": "array", - "description": "Values list for multi-value rules like monitor_type_in", - "nullable": true, - "items": { - "type": "string", - "description": "Values list for multi-value rules like monitor_type_in", - "nullable": true - } - } - }, - "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)" - }, - "UpdateNotificationPolicyRequest": { - "required": [ - "enabled", - "escalation", - "name", - "priority" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this policy" - }, - "matchRules": { - "type": "array", - "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)", - "items": { - "$ref": "#/components/schemas/MatchRule" - } - }, - "escalation": { - "$ref": "#/components/schemas/EscalationChain" - }, - "enabled": { - "type": "boolean", - "description": "Whether this policy is enabled" - }, - "priority": { - "type": "integer", - "description": "Evaluation priority; higher value = evaluated first", - "format": "int32" - } - }, - "description": "Request body for updating a notification policy" - }, - "NotificationPolicyDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique notification policy identifier", - "format": "uuid" - }, - "organizationId": { - "type": "integer", - "description": "Organization this policy belongs to", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable name for this policy" - }, - "matchRules": { - "type": "array", - "description": "Match rules (all must pass; empty = catch-all)", - "items": { - "$ref": "#/components/schemas/MatchRule" - } - }, - "escalation": { - "$ref": "#/components/schemas/EscalationChain" - }, - "enabled": { - "type": "boolean", - "description": "Whether this policy is active" - }, - "priority": { - "type": "integer", - "description": "Evaluation order; higher value = evaluated first", - "format": "int32" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the policy was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the policy was last updated", - "format": "date-time" - } - }, - "description": "Org-level notification policy with match rules and escalation chain" - }, - "SingleValueResponseNotificationPolicyDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/NotificationPolicyDto" - } - } - }, - "ConfirmationPolicy": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "How incident confirmation is coordinated across regions", - "enum": [ - "multi_region" - ] - }, - "minRegionsFailing": { - "type": "integer", - "description": "Minimum failing regions required to confirm an incident", - "format": "int32" - }, - "maxWaitSeconds": { - "type": "integer", - "description": "Maximum seconds to wait for enough regions to fail after first trigger", - "format": "int32" - } - }, - "description": "Multi-region confirmation settings" - }, - "IncidentPolicyDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique incident policy identifier", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "description": "Monitor this policy is attached to", - "format": "uuid" - }, - "triggerRules": { - "type": "array", - "description": "Array of trigger rules defining when an incident should be raised", - "items": { - "$ref": "#/components/schemas/TriggerRule" - } - }, - "confirmation": { - "$ref": "#/components/schemas/ConfirmationPolicy" - }, - "recovery": { - "$ref": "#/components/schemas/RecoveryPolicy" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the policy was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the policy was last updated", - "format": "date-time" - }, - "monitorRegionCount": { - "type": "integer", - "description": "Number of regions configured on the monitor (only set in internal API responses)", - "format": "int32", - "nullable": true - }, - "checkFrequencySeconds": { - "type": "integer", - "description": "Monitor check frequency in seconds (only set in internal API responses)", - "format": "int32", - "nullable": true - } - }, - "description": "Incident detection, confirmation, and recovery policy for a monitor" - }, - "RecoveryPolicy": { - "type": "object", - "properties": { - "consecutiveSuccesses": { - "type": "integer", - "description": "Consecutive passing checks required to auto-resolve the incident", - "format": "int32" - }, - "minRegionsPassing": { - "type": "integer", - "description": "Minimum regions that must be passing before recovery can complete", - "format": "int32" - }, - "cooldownMinutes": { - "type": "integer", - "description": "Minutes after resolve before a new incident may open on the same monitor", - "format": "int32" - } - }, - "description": "Auto-recovery settings" - }, - "TriggerRule": { - "required": [ - "scope", - "severity", - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Condition that opens or escalates an incident from check results", - "enum": [ - "consecutive_failures", - "failures_in_window", - "response_time" - ] - }, - "count": { - "type": "integer", - "description": "Failure count for consecutive or windowed failure rules", - "format": "int32", - "nullable": true - }, - "windowMinutes": { - "type": "integer", - "description": "Window length in minutes for failures-in-window rules", - "format": "int32", - "nullable": true - }, - "scope": { - "type": "string", - "description": "Whether the rule applies per region or across regions", - "nullable": true, - "enum": [ - "per_region", - "any_region" - ] - }, - "thresholdMs": { - "type": "integer", - "description": "Response time threshold in milliseconds for response-time rules", - "format": "int32", - "nullable": true - }, - "severity": { - "type": "string", - "description": "Incident severity when this rule fires", - "enum": [ - "down", - "degraded" - ] - }, - "aggregationType": { - "type": "string", - "description": "How response times are aggregated for response-time rules", - "nullable": true, - "enum": [ - "all_exceed", - "average", - "p95", - "max" - ] - } - }, - "description": "Array of trigger rules defining when an incident should be raised" - }, - "UpdateIncidentPolicyRequest": { - "required": [ - "confirmation", - "recovery", - "triggerRules" - ], - "type": "object", - "properties": { - "triggerRules": { - "minItems": 1, - "type": "array", - "description": "Array of trigger rules; at least one required", - "items": { - "$ref": "#/components/schemas/TriggerRule" - } - }, - "confirmation": { - "$ref": "#/components/schemas/ConfirmationPolicy" - }, - "recovery": { - "$ref": "#/components/schemas/RecoveryPolicy" - } - }, - "description": "Request body for updating an incident policy" - }, - "SingleValueResponseIncidentPolicyDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/IncidentPolicyDto" - } - } - }, - "ApiKeyAuthConfig": { - "required": [ - "headerName" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string", - "description": "HTTP header name that carries the API key" - }, - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID for the API key value", - "format": "uuid", - "nullable": true - } - } - } - ] - }, - "BasicAuthConfig": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID holding Basic auth username and password", - "format": "uuid", - "nullable": true - } - } - } - ] - }, - "BearerAuthConfig": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID holding the bearer token value", - "format": "uuid", - "nullable": true - } - } - } - ] - }, - "HeaderAuthConfig": { - "required": [ - "headerName" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorAuthConfig" - }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "pattern": "^[A-Za-z0-9\\-_]+$", - "type": "string", - "description": "Custom HTTP header name for the secret value" - }, - "vaultSecretId": { - "type": "string", - "description": "Vault secret ID for the header value", - "format": "uuid", - "nullable": true - } - } - } - ] - }, - "MonitorAuthConfig": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string" - } - }, - "description": "New authentication configuration (full replacement)", - "discriminator": { - "propertyName": "type" - } - }, - "UpdateMonitorAuthRequest": { - "required": [ - "config" - ], - "type": "object", - "properties": { - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - } - } - }, - "MonitorAuthDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "format": "uuid" - }, - "authType": { - "type": "string", - "enum": [ - "bearer", - "basic", - "header", - "api_key" - ] - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - } - } - }, - "SingleValueResponseMonitorAuthDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MonitorAuthDto" - } - } - }, - "AssertionConfig": { - "required": [ - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string" - } - }, - "description": "New assertion configuration (full replacement)", - "discriminator": { - "propertyName": "type" - } - }, - "BodyContainsAssertion": { - "required": [ - "substring" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in the response body" - } - } - } - ] - }, - "DnsExpectedCnameAssertion": { - "required": [ - "value" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "value": { - "minLength": 1, - "type": "string", - "description": "Expected CNAME target the resolution must include" - } - } - } - ] - }, - "DnsExpectedIpsAssertion": { - "required": [ - "ips" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "ips": { - "minItems": 1, - "type": "array", - "description": "Allowed IP addresses; at least one resolved address must match", - "items": { - "type": "string", - "description": "Allowed IP addresses; at least one resolved address must match" - } - } - } - } - ] - }, - "DnsMaxAnswersAssertion": { - "required": [ - "recordType" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type whose answer count is checked" - }, - "max": { - "type": "integer", - "description": "Maximum number of answers allowed for that record type", - "format": "int32" - } - } - } - ] - }, - "DnsMinAnswersAssertion": { - "required": [ - "recordType" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type whose answer count is checked" - }, - "min": { - "type": "integer", - "description": "Minimum number of answers required for that record type", - "format": "int32" - } - } - } - ] - }, - "DnsRecordContainsAssertion": { - "required": [ - "recordType", - "substring" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" - }, - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in a matching record value" - } - } - } - ] - }, - "DnsRecordEqualsAssertion": { - "required": [ - "recordType", - "value" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "recordType": { - "minLength": 1, - "type": "string", - "description": "DNS record type to assert on (A, AAAA, CNAME, MX, TXT)" - }, - "value": { - "minLength": 1, - "type": "string", - "description": "Expected DNS record value for an exact match" - } - } - } - ] - }, - "DnsResolvesAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - } - ] - }, - "DnsResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum allowed DNS resolution time in milliseconds", - "format": "int32" - } - } - } - ] - }, - "DnsResponseTimeWarnAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "DNS resolution time in milliseconds that triggers a warning only", - "format": "int32" - } - } - } - ] - }, - "DnsTtlHighAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxTtl": { - "type": "integer", - "description": "Maximum TTL in seconds before a high-TTL warning is raised", - "format": "int32" - } - } - } - ] - }, - "DnsTtlLowAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "minTtl": { - "type": "integer", - "description": "Minimum acceptable TTL in seconds before a warning is raised", - "format": "int32" - } - } - } - ] - }, - "DnsTxtContainsAssertion": { - "required": [ - "substring" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "substring": { - "minLength": 1, - "type": "string", - "description": "Substring that must appear in at least one TXT record" - } - } - } - ] - }, - "HeaderValueAssertion": { - "required": [ - "expected", - "headerName", - "operator" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "headerName": { - "minLength": 1, - "type": "string", - "description": "HTTP header name to assert on" - }, - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected value to compare against" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - } - } - ] - }, - "HeartbeatIntervalDriftAssertion": { - "required": [ - "maxDeviationPercent" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxDeviationPercent": { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "Max percent drift from expected ping interval before warning (non-fatal)", - "format": "int32" - } - } - } - ] - }, - "HeartbeatMaxIntervalAssertion": { - "required": [ - "maxSeconds" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxSeconds": { - "minimum": 1, - "type": "integer", - "description": "Maximum allowed gap in seconds between consecutive heartbeat pings", - "format": "int32" - } - } - } - ] - }, - "HeartbeatPayloadContainsAssertion": { - "required": [ - "path", - "value" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "path": { - "minLength": 1, - "type": "string", - "description": "JSONPath expression into the heartbeat ping JSON payload" - }, - "value": { - "type": "string", - "description": "Expected value to compare against at that path" - } - } - } - ] - }, - "HeartbeatReceivedAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - } - ] - }, - "IcmpPacketLossAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxPercent": { - "maximum": 100.0, - "exclusiveMaximum": false, - "minimum": 0.0, - "exclusiveMinimum": false, - "type": "number", - "description": "Maximum allowed packet loss percentage before the check fails (0\u2013100)", - "format": "double" - } - } - } - ] - }, - "IcmpReachableAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - } - ] - }, - "IcmpResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum average ICMP round-trip time in milliseconds", - "format": "int32" - } - } - } - ] - }, - "IcmpResponseTimeWarnAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "ICMP round-trip time in milliseconds that triggers a warning only", - "format": "int32" - } - } - } - ] - }, - "JsonPathAssertion": { - "required": [ - "expected", - "operator", - "path" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "path": { - "minLength": 1, - "type": "string", - "description": "JSONPath expression to extract a value from the response body" - }, - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected value to compare against" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - } - } - ] - }, - "McpConnectsAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - } - ] - }, - "McpHasCapabilityAssertion": { - "required": [ - "capability" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "capability": { - "minLength": 1, - "type": "string", - "description": "Capability name the server must advertise, e.g. tools or resources" - } - } - } - ] - }, - "McpMinToolsAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "min": { - "type": "integer", - "description": "Minimum number of tools the server must expose", - "format": "int32" - } - } - } - ] - }, - "McpProtocolVersionAssertion": { - "required": [ - "version" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "version": { - "minLength": 1, - "type": "string", - "description": "Expected MCP protocol version string from the server handshake" - } - } - } - ] - }, - "McpResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum allowed MCP check duration in milliseconds", - "format": "int32" - } - } - } - ] - }, - "McpResponseTimeWarnAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "MCP check duration in milliseconds that triggers a warning only", - "format": "int32" - } - } - } - ] - }, - "McpToolAvailableAssertion": { - "required": [ - "toolName" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "toolName": { - "minLength": 1, - "type": "string", - "description": "MCP tool name that must appear in the server's tool list" - } - } - } - ] - }, - "McpToolCountChangedAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "expectedCount": { - "type": "integer", - "description": "Expected tool count; warns when the live count differs", - "format": "int32" - } - } - } - ] - }, - "RedirectCountAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxCount": { - "type": "integer", - "description": "Maximum number of HTTP redirects allowed before the check fails", - "format": "int32" - } - } - } - ] - }, - "RedirectTargetAssertion": { - "required": [ - "expected", - "operator" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected final URL after following redirects" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - } - } - ] - }, - "RegexBodyAssertion": { - "required": [ - "pattern" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "pattern": { - "minLength": 1, - "type": "string", - "description": "Regular expression the response body must match" - } - } - } - ] - }, - "ResponseSizeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxBytes": { - "type": "integer", - "description": "Maximum response body size in bytes before the check fails", - "format": "int32" - } - } - } - ] - }, - "ResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "thresholdMs": { - "type": "integer", - "description": "Maximum allowed response time in milliseconds before the check fails", - "format": "int32" - } - } - } - ] - }, - "ResponseTimeWarnAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "HTTP response time in milliseconds that triggers a warning only", - "format": "int32" - } - } - } - ] - }, - "SslExpiryAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "minDaysRemaining": { - "type": "integer", - "description": "Minimum days before TLS certificate expiry; fails or warns below this threshold", - "format": "int32" - } - } - } - ] - }, - "StatusCodeAssertion": { - "required": [ - "expected", - "operator" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "expected": { - "minLength": 1, - "type": "string", - "description": "Expected status code, range pattern, or wildcard such as 2xx" - }, - "operator": { - "type": "string", - "description": "Comparison operator (equals, contains, less_than, greater_than, etc.)", - "enum": [ - "equals", - "contains", - "less_than", - "greater_than", - "matches", - "range" - ] - } - } - } - ] - }, - "TcpConnectsAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - } - ] - }, - "TcpResponseTimeAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "maxMs": { - "type": "integer", - "description": "Maximum TCP connect time in milliseconds before the check fails", - "format": "int32" - } - } - } - ] - }, - "TcpResponseTimeWarnAssertion": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/AssertionConfig" - }, - { - "type": "object", - "properties": { - "warnMs": { - "type": "integer", - "description": "TCP connect time in milliseconds that triggers a warning only", - "format": "int32" - } - } - } - ] - }, - "UpdateAssertionRequest": { - "required": [ - "config" - ], - "type": "object", - "properties": { - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/BodyContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedCnameAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedIpsAssertion" - }, - { - "$ref": "#/components/schemas/DnsMaxAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsMinAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordEqualsAssertion" - }, - { - "$ref": "#/components/schemas/DnsResolvesAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlHighAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlLowAssertion" - }, - { - "$ref": "#/components/schemas/DnsTxtContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeaderValueAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatReceivedAssertion" - }, - { - "$ref": "#/components/schemas/IcmpPacketLossAssertion" - }, - { - "$ref": "#/components/schemas/IcmpReachableAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/JsonPathAssertion" - }, - { - "$ref": "#/components/schemas/McpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/McpHasCapabilityAssertion" - }, - { - "$ref": "#/components/schemas/McpMinToolsAssertion" - }, - { - "$ref": "#/components/schemas/McpProtocolVersionAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/McpToolAvailableAssertion" - }, - { - "$ref": "#/components/schemas/McpToolCountChangedAssertion" - }, - { - "$ref": "#/components/schemas/RedirectCountAssertion" - }, - { - "$ref": "#/components/schemas/RedirectTargetAssertion" - }, - { - "$ref": "#/components/schemas/RegexBodyAssertion" - }, - { - "$ref": "#/components/schemas/ResponseSizeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/SslExpiryAssertion" - }, - { - "$ref": "#/components/schemas/StatusCodeAssertion" - }, - { - "$ref": "#/components/schemas/TcpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" - } - ] - }, - "severity": { - "type": "string", - "description": "New outcome severity: FAIL or WARN", - "enum": [ - "fail", - "warn" - ] - } - } - }, - "MonitorAssertionDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "format": "uuid" - }, - "assertionType": { - "type": "string", - "enum": [ - "status_code", - "response_time", - "body_contains", - "json_path", - "header", - "regex", - "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": { - "oneOf": [ - { - "$ref": "#/components/schemas/BodyContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedCnameAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedIpsAssertion" - }, - { - "$ref": "#/components/schemas/DnsMaxAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsMinAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordEqualsAssertion" - }, - { - "$ref": "#/components/schemas/DnsResolvesAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlHighAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlLowAssertion" - }, - { - "$ref": "#/components/schemas/DnsTxtContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeaderValueAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatReceivedAssertion" - }, - { - "$ref": "#/components/schemas/IcmpPacketLossAssertion" - }, - { - "$ref": "#/components/schemas/IcmpReachableAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/JsonPathAssertion" - }, - { - "$ref": "#/components/schemas/McpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/McpHasCapabilityAssertion" - }, - { - "$ref": "#/components/schemas/McpMinToolsAssertion" - }, - { - "$ref": "#/components/schemas/McpProtocolVersionAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/McpToolAvailableAssertion" - }, - { - "$ref": "#/components/schemas/McpToolCountChangedAssertion" - }, - { - "$ref": "#/components/schemas/RedirectCountAssertion" - }, - { - "$ref": "#/components/schemas/RedirectTargetAssertion" - }, - { - "$ref": "#/components/schemas/RegexBodyAssertion" - }, - { - "$ref": "#/components/schemas/ResponseSizeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/SslExpiryAssertion" - }, - { - "$ref": "#/components/schemas/StatusCodeAssertion" - }, - { - "$ref": "#/components/schemas/TcpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" - } - ] - }, - "severity": { - "type": "string", - "enum": [ - "fail", - "warn" - ] - } - } - }, - "SingleValueResponseMonitorAssertionDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MonitorAssertionDto" - } - } - }, - "SetAlertChannelsRequest": { - "required": [ - "channelIds" - ], - "type": "object", - "properties": { - "channelIds": { - "type": "array", - "description": "IDs of alert channels to link (replaces current list)", - "items": { - "type": "string", - "description": "IDs of alert channels to link (replaces current list)", - "format": "uuid" - } - } - }, - "description": "Replace the alert channels linked to a monitor" - }, - "SingleValueResponseListUUID": { - "type": "object", - "properties": { - "data": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "format": "uuid", - "nullable": true - } - } - } - }, - "AddMonitorTagsRequest": { - "type": "object", - "properties": { - "tagIds": { - "type": "array", - "description": "IDs of existing org tags to attach", - "nullable": true, - "items": { - "type": "string", - "description": "IDs of existing org tags to attach", - "format": "uuid", - "nullable": true - } - }, - "newTags": { - "type": "array", - "description": "New tags to create (if not already present) and attach", - "nullable": true, - "items": { - "$ref": "#/components/schemas/NewTagRequest" - } - } - }, - "description": "Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both." - }, - "CreateAssertionRequest": { - "required": [ - "config" - ], - "type": "object", - "properties": { - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/BodyContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedCnameAssertion" - }, - { - "$ref": "#/components/schemas/DnsExpectedIpsAssertion" - }, - { - "$ref": "#/components/schemas/DnsMaxAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsMinAnswersAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordContainsAssertion" - }, - { - "$ref": "#/components/schemas/DnsRecordEqualsAssertion" - }, - { - "$ref": "#/components/schemas/DnsResolvesAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/DnsResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlHighAssertion" - }, - { - "$ref": "#/components/schemas/DnsTtlLowAssertion" - }, - { - "$ref": "#/components/schemas/DnsTxtContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeaderValueAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatIntervalDriftAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatMaxIntervalAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatPayloadContainsAssertion" - }, - { - "$ref": "#/components/schemas/HeartbeatReceivedAssertion" - }, - { - "$ref": "#/components/schemas/IcmpPacketLossAssertion" - }, - { - "$ref": "#/components/schemas/IcmpReachableAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/IcmpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/JsonPathAssertion" - }, - { - "$ref": "#/components/schemas/McpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/McpHasCapabilityAssertion" - }, - { - "$ref": "#/components/schemas/McpMinToolsAssertion" - }, - { - "$ref": "#/components/schemas/McpProtocolVersionAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/McpResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/McpToolAvailableAssertion" - }, - { - "$ref": "#/components/schemas/McpToolCountChangedAssertion" - }, - { - "$ref": "#/components/schemas/RedirectCountAssertion" - }, - { - "$ref": "#/components/schemas/RedirectTargetAssertion" - }, - { - "$ref": "#/components/schemas/RegexBodyAssertion" - }, - { - "$ref": "#/components/schemas/ResponseSizeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/ResponseTimeWarnAssertion" - }, - { - "$ref": "#/components/schemas/SslExpiryAssertion" - }, - { - "$ref": "#/components/schemas/StatusCodeAssertion" - }, - { - "$ref": "#/components/schemas/TcpConnectsAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeAssertion" - }, - { - "$ref": "#/components/schemas/TcpResponseTimeWarnAssertion" - } - ] - }, - "severity": { - "type": "string", - "description": "Outcome severity: FAIL (fails the check) or WARN (warns without failing)", - "enum": [ - "fail", - "warn" - ] - } - }, - "description": "Replace all assertions; null preserves current" - }, - "DnsMonitorConfig": { - "required": [ - "hostname" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "hostname": { - "minLength": 1, - "type": "string", - "description": "Domain name to resolve" - }, - "recordTypes": { - "type": "array", - "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", - "nullable": true, - "items": { - "type": "string", - "description": "DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR", - "nullable": true, - "enum": [ - "A", - "AAAA", - "CNAME", - "MX", - "NS", - "TXT", - "SRV", - "SOA", - "CAA", - "PTR" - ] - } - }, - "nameservers": { - "type": "array", - "description": "Custom nameservers to query (uses system defaults if omitted)", - "nullable": true, - "items": { - "type": "string", - "description": "Custom nameservers to query (uses system defaults if omitted)", - "nullable": true - } - }, - "timeoutMs": { - "type": "integer", - "description": "Per-query timeout in milliseconds", - "format": "int32", - "nullable": true - }, - "totalTimeoutMs": { - "type": "integer", - "description": "Total timeout for all queries in milliseconds", - "format": "int32", - "nullable": true - } - } - } - ] - }, - "HeartbeatMonitorConfig": { - "required": [ - "expectedInterval", - "gracePeriod" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "expectedInterval": { - "maximum": 86400, - "minimum": 1, - "type": "integer", - "description": "Expected heartbeat interval in seconds", - "format": "int32" - }, - "gracePeriod": { - "minimum": 1, - "type": "integer", - "description": "Grace period in seconds before marking as down", - "format": "int32" - } - } - } - ] - }, - "HttpMonitorConfig": { - "required": [ - "method", - "url" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Target URL to send requests to" - }, - "method": { - "type": "string", - "description": "HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE", - "HEAD" - ] - }, - "customHeaders": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Additional HTTP headers to include in requests", - "nullable": true - }, - "description": "Additional HTTP headers to include in requests", - "nullable": true - }, - "requestBody": { - "type": "string", - "description": "Request body content for POST/PUT/PATCH methods", - "nullable": true - }, - "contentType": { - "type": "string", - "description": "Content-Type header value for the request body", - "nullable": true - }, - "verifyTls": { - "type": "boolean", - "description": "Whether to verify TLS certificates (default: true)", - "nullable": true - } - } - } - ] - }, - "IcmpMonitorConfig": { - "required": [ - "host" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "host": { - "minLength": 1, - "type": "string", - "description": "Target hostname or IP address to ping" - }, - "packetCount": { - "maximum": 20, - "minimum": 1, - "type": "integer", - "description": "Number of ICMP packets to send", - "format": "int32", - "nullable": true - }, - "timeoutMs": { - "type": "integer", - "description": "Ping timeout in milliseconds", - "format": "int32", - "nullable": true - } - } - } - ] - }, - "McpServerMonitorConfig": { - "required": [ - "command" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "command": { - "minLength": 1, - "type": "string", - "description": "Command to execute to start the MCP server" - }, - "args": { - "type": "array", - "description": "Command-line arguments for the MCP server process", - "nullable": true, - "items": { - "type": "string", - "description": "Command-line arguments for the MCP server process", - "nullable": true - } - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Environment variables to pass to the MCP server process", - "nullable": true - }, - "description": "Environment variables to pass to the MCP server process", - "nullable": true - } - } - } - ] - }, - "MonitorConfig": { - "type": "object", - "description": "Updated protocol-specific configuration; null preserves current" - }, - "NewTagRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "Tag name" - }, - "color": { - "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", - "type": "string", - "description": "Hex color code (defaults to #6B7280 if omitted)", - "nullable": true - } - }, - "description": "Inline tag creation \u2014 creates the tag if it does not already exist" - }, - "TcpMonitorConfig": { - "required": [ - "host" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MonitorConfig" - }, - { - "type": "object", - "properties": { - "host": { - "minLength": 1, - "type": "string", - "description": "Target hostname or IP address" - }, - "port": { - "maximum": 65535, - "minimum": 1, - "type": "integer", - "description": "TCP port to connect to", - "format": "int32" - }, - "timeoutMs": { - "type": "integer", - "description": "Connection timeout in milliseconds", - "format": "int32", - "nullable": true - } - } - } - ] - }, - "UpdateMonitorRequest": { - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "New monitor name; null preserves current", - "nullable": true - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] - }, - "frequencySeconds": { - "type": "integer", - "description": "New check frequency in seconds (30\u201386400); null preserves current", - "format": "int32", - "nullable": true - }, - "enabled": { - "type": "boolean", - "description": "Enable or disable the monitor; null preserves current", - "nullable": true - }, - "regions": { - "type": "array", - "description": "New probe regions; null preserves current", - "nullable": true, - "items": { - "type": "string", - "description": "New probe regions; null preserves current", - "nullable": true - } - }, - "managedBy": { - "type": "string", - "description": "New management source; null preserves current", - "nullable": true, - "enum": [ - "DASHBOARD", - "CLI" - ] - }, - "environmentId": { - "type": "string", - "description": "New environment ID; null preserves current (use clearEnvironmentId to unset)", - "format": "uuid", - "nullable": true - }, - "clearEnvironmentId": { - "type": "boolean", - "description": "Set to true to remove the environment association", - "nullable": true - }, - "assertions": { - "type": "array", - "description": "Replace all assertions; null preserves current", - "nullable": true, - "items": { - "$ref": "#/components/schemas/CreateAssertionRequest" - } - }, - "auth": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - }, - "clearAuth": { - "type": "boolean", - "description": "Set to true to remove authentication", - "nullable": true - }, - "incidentPolicy": { - "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" - }, - "alertChannelIds": { - "type": "array", - "description": "Replace alert channel list; null preserves current", - "nullable": true, - "items": { - "type": "string", - "description": "Replace alert channel list; null preserves current", - "format": "uuid", - "nullable": true - } - }, - "tags": { - "$ref": "#/components/schemas/AddMonitorTagsRequest" - } - } - }, - "MonitorDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique monitor identifier", - "format": "uuid" - }, - "organizationId": { - "type": "integer", - "description": "Organization this monitor belongs to", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable name for this monitor" - }, - "type": { - "type": "string", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] - }, - "frequencySeconds": { - "type": "integer", - "description": "Check frequency in seconds (30\u201386400)", - "format": "int32" - }, - "enabled": { - "type": "boolean", - "description": "Whether the monitor is active" - }, - "regions": { - "type": "array", - "description": "Probe regions where checks are executed", - "items": { - "type": "string", - "description": "Probe regions where checks are executed" - } - }, - "managedBy": { - "type": "string", - "description": "Management source: DASHBOARD or CLI", - "enum": [ - "DASHBOARD", - "CLI" - ] - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the monitor was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the monitor was last updated", - "format": "date-time" - }, - "assertions": { - "type": "array", - "description": "Assertions evaluated against each check result; null on list responses", - "nullable": true, - "items": { - "$ref": "#/components/schemas/MonitorAssertionDto" - } - }, - "tags": { - "type": "array", - "description": "Tags applied to this monitor", - "nullable": true, - "items": { - "$ref": "#/components/schemas/TagDto" - } - }, - "pingUrl": { - "type": "string", - "description": "Heartbeat ping URL; populated for HEARTBEAT monitors only", - "nullable": true - }, - "environment": { - "$ref": "#/components/schemas/Summary" - }, - "auth": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - }, - "incidentPolicy": { - "$ref": "#/components/schemas/IncidentPolicyDto" - }, - "alertChannelIds": { - "type": "array", - "description": "Alert channel IDs linked to this monitor; populated on single-monitor responses", - "nullable": true, - "items": { - "type": "string", - "description": "Alert channel IDs linked to this monitor; populated on single-monitor responses", - "format": "uuid", - "nullable": true - } - } - }, - "description": "Full monitor representation" - }, - "SingleValueResponseMonitorDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MonitorDto" - } - } - }, - "Summary": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "slug": { - "type": "string" - } - }, - "description": "Environment associated with this monitor; null when unassigned" - }, - "ChangeStatusRequest": { - "required": [ - "status" - ], - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "New membership status (ACTIVE or SUSPENDED)", - "enum": [ - "INVITED", - "ACTIVE", - "SUSPENDED", - "LEFT", - "REMOVED", - "DECLINED" - ] - } - }, - "description": "Update an organization member's status" - }, - "UpdateMaintenanceWindowRequest": { - "required": [ - "endsAt", - "startsAt" - ], - "type": "object", - "properties": { - "monitorId": { - "type": "string", - "description": "Monitor to attach this maintenance window to; null preserves current", - "format": "uuid" - }, - "startsAt": { - "type": "string", - "description": "Updated start time (ISO 8601)", - "format": "date-time" - }, - "endsAt": { - "type": "string", - "description": "Updated end time (ISO 8601)", - "format": "date-time" - }, - "repeatRule": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "Updated iCal RRULE; null clears the repeat rule" - }, - "reason": { - "type": "string", - "description": "Updated reason; null clears the existing reason" - }, - "suppressAlerts": { - "type": "boolean", - "description": "Whether to suppress alerts; null preserves current" - } - } - }, - "MaintenanceWindowDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique maintenance window identifier", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "description": "Monitor this window applies to; null for org-wide windows", - "format": "uuid", - "nullable": true - }, - "organizationId": { - "type": "integer", - "description": "Organization this maintenance window belongs to", - "format": "int32" - }, - "startsAt": { - "type": "string", - "description": "Scheduled start of the maintenance window", - "format": "date-time" - }, - "endsAt": { - "type": "string", - "description": "Scheduled end of the maintenance window", - "format": "date-time" - }, - "repeatRule": { - "type": "string", - "description": "iCal RRULE for recurring windows; null for one-time", - "nullable": true - }, - "reason": { - "type": "string", - "description": "Human-readable reason for the maintenance", - "nullable": true - }, - "suppressAlerts": { - "type": "boolean", - "description": "Whether alerts are suppressed during this window" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the window was created", - "format": "date-time" - } - }, - "description": "Scheduled maintenance window for a monitor" - }, - "SingleValueResponseMaintenanceWindowDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MaintenanceWindowDto" - } - } - }, - "UpdateEnvironmentRequest": { - "type": "object", - "properties": { - "name": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "New environment name; null preserves current", - "nullable": true - }, - "variables": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Replace all variables; null preserves current", - "nullable": true - }, - "description": "Replace all variables; null preserves current", - "nullable": true - }, - "isDefault": { - "type": "boolean", - "description": "Whether this is the default environment; null preserves current", - "nullable": true - } - } - }, - "EnvironmentDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique environment identifier", - "format": "uuid" - }, - "orgId": { - "type": "integer", - "description": "Organization this environment belongs to", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable environment name" - }, - "slug": { - "type": "string", - "description": "URL-safe identifier" - }, - "variables": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Key-value variable pairs available for interpolation" - }, - "description": "Key-value variable pairs available for interpolation" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the environment was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the environment was last updated", - "format": "date-time" - }, - "monitorCount": { - "type": "integer", - "description": "Number of monitors using this environment", - "format": "int32" - }, - "isDefault": { - "type": "boolean", - "description": "Whether this is the default environment for new monitors" - } - }, - "description": "Environment with variable substitutions for monitor configs" - }, - "SingleValueResponseEnvironmentDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/EnvironmentDto" - } - } - }, - "ChannelConfig": { - "required": [ - "channelType" - ], - "type": "object", - "properties": { - "channelType": { - "type": "string" - } - }, - "description": "New channel configuration (full replacement, not partial update)", - "discriminator": { - "propertyName": "channelType" - } - }, - "DiscordChannelConfig": { - "required": [ - "webhookUrl" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Discord webhook URL" - }, - "mentionRoleId": { - "type": "string", - "description": "Optional Discord role ID to mention in notifications", - "nullable": true - } - } - } - ] - }, - "EmailChannelConfig": { - "required": [ - "recipients" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "recipients": { - "minItems": 1, - "type": "array", - "description": "Email addresses to send notifications to", - "items": { - "type": "string", - "description": "Email addresses to send notifications to", - "format": "email" - } - } - } - } - ] - }, - "OpsGenieChannelConfig": { - "required": [ - "apiKey" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "apiKey": { - "minLength": 1, - "type": "string", - "description": "OpsGenie API key for alert creation" - }, - "region": { - "type": "string", - "description": "OpsGenie API region: us or eu", - "nullable": true - } - } - } - ] - }, - "PagerDutyChannelConfig": { - "required": [ - "routingKey" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "routingKey": { - "minLength": 1, - "type": "string", - "description": "PagerDuty Events API v2 routing (integration) key" - }, - "severityOverride": { - "type": "string", - "description": "Override PagerDuty severity mapping", - "nullable": true - } - } - } - ] - }, - "SlackChannelConfig": { - "required": [ - "webhookUrl" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Slack incoming webhook URL" - }, - "mentionText": { - "type": "string", - "description": "Optional mention text included in notifications, e.g. @channel", - "nullable": true - } - } - } - ] - }, - "TeamsChannelConfig": { - "required": [ - "webhookUrl" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "webhookUrl": { - "minLength": 1, - "type": "string", - "description": "Microsoft Teams incoming webhook URL" - } - } - } - ] - }, - "UpdateAlertChannelRequest": { - "required": [ - "config", - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "New channel name (full replacement, not partial update)" - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DiscordChannelConfig" - }, - { - "$ref": "#/components/schemas/EmailChannelConfig" - }, - { - "$ref": "#/components/schemas/OpsGenieChannelConfig" - }, - { - "$ref": "#/components/schemas/PagerDutyChannelConfig" - }, - { - "$ref": "#/components/schemas/SlackChannelConfig" - }, - { - "$ref": "#/components/schemas/TeamsChannelConfig" - }, - { - "$ref": "#/components/schemas/WebhookChannelConfig" - } - ] - } - } - }, - "WebhookChannelConfig": { - "required": [ - "url" - ], - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/ChannelConfig" - }, - { - "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Webhook endpoint URL that receives alert payloads" - }, - "signingSecret": { - "type": "string", - "description": "Optional HMAC signing secret for payload verification", - "nullable": true - }, - "customHeaders": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Additional HTTP headers to include in webhook requests", - "nullable": true - }, - "description": "Additional HTTP headers to include in webhook requests", - "nullable": true - } - } - } - ] - }, - "AlertChannelDto": { - "required": [ - "channelType", - "createdAt", - "id", - "name", - "updatedAt" - ], - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique alert channel identifier", - "format": "uuid" - }, - "name": { - "type": "string", - "description": "Human-readable channel name" - }, - "channelType": { - "type": "string", - "description": "Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL)", - "enum": [ - "email", - "webhook", - "slack", - "pagerduty", - "opsgenie", - "teams", - "discord" - ] - }, - "displayConfig": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Non-sensitive display metadata; null for older channels", - "nullable": true - }, - "description": "Non-sensitive display metadata; null for older channels", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the channel was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the channel was last updated", - "format": "date-time" - }, - "configHash": { - "type": "string", - "description": "SHA-256 hash of the channel config; use for change detection", - "nullable": true - }, - "lastDeliveryAt": { - "type": "string", - "description": "Timestamp of the most recent delivery attempt", - "format": "date-time", - "nullable": true - }, - "lastDeliveryStatus": { - "type": "string", - "description": "Outcome of the most recent delivery (SUCCESS, FAILED, etc.)", - "nullable": true - } - }, - "description": "Alert channel with non-sensitive configuration metadata" - }, - "SingleValueResponseAlertChannelDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AlertChannelDto" - } - } - }, - "WorkspaceCreateParams": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "organizationId": { - "type": "integer", - "format": "int32" - }, - "name": { - "minLength": 1, - "type": "string" - } - } - }, - "ServiceIncidentRequest": { - "required": [ - "action", - "externalRef", - "serviceId", - "title" - ], - "type": "object", - "properties": { - "serviceId": { - "type": "string", - "format": "uuid" - }, - "externalRef": { - "minLength": 1, - "type": "string" - }, - "severity": { - "type": "string", - "nullable": true - }, - "title": { - "minLength": 1, - "type": "string" - }, - "shortlink": { - "type": "string", - "nullable": true - }, - "affectedComponents": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - }, - "serviceIncidentId": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "action": { - "minLength": 1, - "type": "string" - }, - "statusText": { - "type": "string", - "nullable": true - } - } - }, - "IncidentDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique incident identifier", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "description": "Monitor that triggered the incident; null for service or manual incidents", - "format": "uuid", - "nullable": true - }, - "organizationId": { - "type": "integer", - "description": "Organization this incident belongs to", - "format": "int32" - }, - "source": { - "type": "string", - "description": "Incident origin: MONITOR, SERVICE, or MANUAL", - "enum": [ - "AUTOMATIC", - "MANUAL", - "MONITORS", - "STATUS_DATA", - "RESOURCE_GROUP" - ] - }, - "status": { - "type": "string", - "description": "Current lifecycle status (OPEN, RESOLVED, etc.)", - "enum": [ - "WATCHING", - "TRIGGERED", - "CONFIRMED", - "RESOLVED" - ] - }, - "severity": { - "type": "string", - "description": "Severity level: DOWN, DEGRADED, or MAINTENANCE", - "enum": [ - "DOWN", - "DEGRADED", - "MAINTENANCE" - ] - }, - "title": { - "type": "string", - "description": "Short summary of the incident; null for auto-generated incidents", - "nullable": true - }, - "triggeredByRule": { - "type": "string", - "description": "Human-readable description of the trigger rule that fired", - "nullable": true - }, - "affectedRegions": { - "type": "array", - "description": "Probe regions that observed the failure", - "items": { - "type": "string", - "description": "Probe regions that observed the failure" - } - }, - "reopenCount": { - "type": "integer", - "description": "Number of times this incident has been reopened", - "format": "int32" - }, - "createdByUserId": { - "type": "integer", - "description": "User who created the incident (manual incidents only)", - "format": "int32", - "nullable": true - }, - "statusPageVisible": { - "type": "boolean", - "description": "Whether this incident is visible on the status page" - }, - "serviceIncidentId": { - "type": "string", - "description": "Linked vendor service incident ID; null for monitor incidents", - "format": "uuid", - "nullable": true - }, - "serviceId": { - "type": "string", - "description": "Linked service catalog ID; null for monitor incidents", - "format": "uuid", - "nullable": true - }, - "externalRef": { - "type": "string", - "description": "External reference ID (e.g. PagerDuty incident ID)", - "nullable": true - }, - "affectedComponents": { - "type": "array", - "description": "Service components affected by this incident", - "nullable": true, - "items": { - "type": "string", - "description": "Service components affected by this incident", - "nullable": true - } - }, - "shortlink": { - "type": "string", - "description": "Short URL linking to the incident details", - "nullable": true - }, - "resolutionReason": { - "type": "string", - "description": "How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.)", - "nullable": true, - "enum": [ - "MANUAL", - "AUTO_RECOVERED", - "AUTO_RESOLVED" - ] - }, - "startedAt": { - "type": "string", - "description": "Timestamp when the incident was detected or created", - "format": "date-time", - "nullable": true - }, - "confirmedAt": { - "type": "string", - "description": "Timestamp when the incident was confirmed (multi-region confirmation)", - "format": "date-time", - "nullable": true - }, - "resolvedAt": { - "type": "string", - "description": "Timestamp when the incident was resolved", - "format": "date-time", - "nullable": true - }, - "cooldownUntil": { - "type": "string", - "description": "Cooldown window end; new incidents suppressed until this time", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the incident record was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the incident was last updated", - "format": "date-time" - }, - "monitorName": { - "type": "string", - "description": "Name of the associated monitor; populated on list responses", - "nullable": true - }, - "serviceName": { - "type": "string", - "description": "Name of the associated service; populated on list responses", - "nullable": true - }, - "serviceSlug": { - "type": "string", - "description": "Slug of the associated service; populated on list responses", - "nullable": true - }, - "monitorType": { - "type": "string", - "description": "Type of the associated monitor; populated on list responses", - "nullable": true - }, - "resourceGroupId": { - "type": "string", - "description": "Resource group that owns this incident; null when not group-managed", - "format": "uuid", - "nullable": true - }, - "resourceGroupName": { - "type": "string", - "description": "Name of the resource group; populated on list responses", - "nullable": true - } - }, - "description": "Incident triggered by a monitor check failure or manual creation" - }, - "TableValueResultIncidentDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncidentDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseInteger": { - "type": "object", - "properties": { - "data": { - "type": "integer", - "format": "int32", - "nullable": true - } - } - }, - "CreateAutoIncidentRequest": { - "required": [ - "monitorId" - ], - "type": "object", - "properties": { - "monitorId": { - "type": "string", - "format": "uuid" - }, - "severity": { - "type": "string", - "nullable": true - }, - "triggeredByRule": { - "type": "string", - "nullable": true - }, - "affectedRegions": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - }, - "startedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "SingleValueResponseIncidentDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/IncidentDto" - } - } - }, - "ReopenAutoIncidentRequest": { - "type": "object", - "properties": { - "affectedRegions": { - "type": "array", - "items": { - "type": "string" - } - }, - "severity": { - "type": "string", - "nullable": true - } - } - }, - "AdapterHealthReportRequest": { - "required": [ - "serviceId", - "success" - ], - "type": "object", - "properties": { - "serviceId": { - "type": "string", - "format": "uuid" - }, - "success": { - "type": "boolean" - }, - "errorMessage": { - "type": "string", - "nullable": true - } - } - }, - "AdapterHealthDto": { - "type": "object", - "properties": { - "serviceId": { - "type": "string", - "description": "Service this health record belongs to", - "format": "uuid" - }, - "serviceSlug": { - "type": "string", - "description": "URL-safe service identifier" - }, - "serviceName": { - "type": "string", - "description": "Service name" - }, - "adapterType": { - "type": "string", - "description": "Data source adapter type", - "nullable": true - }, - "lastSuccessAt": { - "type": "string", - "description": "Timestamp of the last successful poll", - "format": "date-time", - "nullable": true - }, - "lastFailureAt": { - "type": "string", - "description": "Timestamp of the last failed poll", - "format": "date-time", - "nullable": true - }, - "consecutiveFailures": { - "type": "integer", - "description": "Number of consecutive poll failures", - "format": "int32" - }, - "lastErrorMessage": { - "type": "string", - "description": "Error message from the most recent failure", - "nullable": true - }, - "disabledByHealth": { - "type": "boolean", - "description": "Whether the adapter is disabled due to repeated failures" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when this health record was last updated", - "format": "date-time" - } - } - }, - "SingleValueResponseAdapterHealthDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AdapterHealthDto" - } - } - }, - "CreateOrgRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "minLength": 1, - "type": "string", - "description": "Organization name" - }, - "email": { - "type": "string", - "description": "Billing and contact email address", - "format": "email", - "nullable": true - } - }, - "description": "Create a new organization" - }, - "SingleValueResponseTransactionDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/TransactionDto" - } - } - }, - "TransactionDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Paddle transaction identifier" - }, - "status": { - "type": "string", - "description": "Transaction status (e.g. completed, pending)", - "nullable": true - }, - "currencyCode": { - "type": "string", - "description": "ISO 4217 currency code", - "nullable": true - }, - "invoiceNumber": { - "type": "string", - "description": "Invoice number; null if not invoiced", - "nullable": true - }, - "billedAt": { - "type": "string", - "description": "Timestamp when the transaction was billed", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the transaction was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the transaction was last updated", - "format": "date-time" - }, - "total": { - "type": "string", - "description": "Total amount as a decimal string (including tax)", - "nullable": true - }, - "subtotal": { - "type": "string", - "description": "Subtotal before tax as a decimal string", - "nullable": true - }, - "tax": { - "type": "string", - "description": "Tax amount as a decimal string", - "nullable": true - } - }, - "description": "A billing transaction from Paddle" - }, - "QuickMonitorRequest": { - "required": [ - "url" - ], - "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Target URL to monitor" - }, - "name": { - "type": "string", - "description": "Human-readable monitor name; defaults to the hostname if omitted", - "nullable": true - }, - "frequencySeconds": { - "type": "integer", - "description": "Check frequency in seconds (30\u201386400); defaults to 60", - "format": "int32", - "nullable": true - } - }, - "description": "Minimal request for creating an HTTP monitor quickly" - }, - "OnboardingSetupRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "Organization or team name (max 200 chars)" - }, - "role": { - "maxLength": 50, - "minLength": 0, - "type": "string", - "description": "User's role or job title", - "nullable": true - }, - "teamSize": { - "maxLength": 50, - "minLength": 0, - "type": "string", - "description": "Team size range (e.g. 1-10, 11-50)", - "nullable": true - } - } - }, - "AnalyzeUrlRequest": { - "required": [ - "url" - ], - "type": "object", - "properties": { - "url": { - "minLength": 1, - "type": "string", - "description": "Target URL to analyze (must be a valid HTTP/HTTPS URL)" - } - }, - "description": "URL to analyze for monitor setup suggestions" - }, - "AnalyzeUrlResponse": { - "type": "object", - "properties": { - "reachable": { - "type": "boolean", - "description": "Whether the URL responded during analysis" - }, - "responseTimeMs": { - "type": "integer", - "description": "Response time observed during analysis in milliseconds", - "format": "int64" - }, - "statusCode": { - "type": "integer", - "description": "HTTP status code from the analysis request", - "format": "int32" - }, - "tlsExpiry": { - "type": "string", - "description": "TLS certificate expiry date; null for non-HTTPS or unavailable", - "format": "date-time", - "nullable": true - }, - "tlsDaysRemaining": { - "type": "integer", - "description": "Days until TLS certificate expires; null if not applicable", - "format": "int32", - "nullable": true - }, - "contentType": { - "type": "string", - "description": "Response Content-Type header value", - "nullable": true - }, - "suggestedName": { - "type": "string", - "description": "Suggested monitor name derived from the URL hostname" - }, - "suggestedAssertions": { - "type": "array", - "description": "Recommended assertions based on the URL response", - "items": { - "$ref": "#/components/schemas/SuggestedAssertion" - } - }, - "suggestedFrequencySeconds": { - "type": "integer", - "description": "Suggested check frequency in seconds based on the URL", - "format": "int32" - } - }, - "description": "Analysis of a URL with monitor setup suggestions" - }, - "SingleValueResponseAnalyzeUrlResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AnalyzeUrlResponse" - } - } - }, - "SuggestedAssertion": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Assertion type (e.g. status_code, response_time)" - }, - "operator": { - "type": "string", - "description": "Comparison operator (e.g. equals, less_than)" - }, - "value": { - "type": "string", - "description": "Expected value to compare against" - } - }, - "description": "Recommended assertions based on the URL response" - }, - "AcceptInviteRequest": { - "required": [ - "token" - ], - "type": "object", - "properties": { - "token": { - "minLength": 1, - "type": "string", - "description": "Invite token from the invitation email" - } - }, - "description": "Accept an organization invite using the invite token" - }, - "AcceptInviteDto": { - "type": "object", - "properties": { - "orgId": { - "type": "integer", - "description": "Organization the user joined", - "format": "int32" - }, - "userId": { - "type": "integer", - "description": "User who accepted the invite", - "format": "int32" - }, - "orgRole": { - "type": "string", - "description": "Role assigned to the new member", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - }, - "status": { - "type": "string", - "description": "Initial membership status after joining", - "enum": [ - "INVITED", - "ACTIVE", - "SUSPENDED", - "LEFT", - "REMOVED", - "DECLINED" - ] - } - }, - "description": "Result of accepting an organization invite" - }, - "SingleValueResponseAcceptInviteDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AcceptInviteDto" - } - } - }, - "RegisterUserRequest": { - "type": "object", - "properties": { - "nickname": { - "type": "string", - "description": "User nickname from the identity provider", - "nullable": true - }, - "name": { - "type": "string", - "description": "User display name from the identity provider", - "nullable": true - }, - "picture": { - "type": "string", - "description": "Profile picture URL from the identity provider", - "nullable": true - } - } - }, - "CreateWorkspaceRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "minLength": 1, - "type": "string", - "description": "Workspace name" - } - }, - "description": "Create a new workspace within the organization" - }, - "AddMemberRequest": { - "required": [ - "orgRole", - "userId" - ], - "type": "object", - "properties": { - "userId": { - "type": "integer", - "description": "ID of the user to add", - "format": "int32" - }, - "orgRole": { - "type": "string", - "description": "Role to assign to the new member", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - } - }, - "description": "Add an existing user as a member of the organization" - }, - "MemberDto": { - "type": "object", - "properties": { - "userId": { - "type": "integer", - "description": "User identifier of the member", - "format": "int32" - }, - "email": { - "type": "string", - "description": "Member email address" - }, - "name": { - "type": "string", - "description": "Member display name; null if not set", - "nullable": true - }, - "orgRole": { - "type": "string", - "description": "Member role within this organization (OWNER, ADMIN, MEMBER)", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - }, - "status": { - "type": "string", - "description": "Membership status (ACTIVE, PENDING, SUSPENDED)", - "enum": [ - "INVITED", - "ACTIVE", - "SUSPENDED", - "LEFT", - "REMOVED", - "DECLINED" - ] - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the member was added to the organization", - "format": "date-time" - } - }, - "description": "Organization member with role and status" - }, - "SingleValueResponseMemberDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MemberDto" - } - } - }, - "CreateWebhookEndpointRequest": { - "required": [ - "subscribedEvents", - "url" - ], - "type": "object", - "properties": { - "url": { - "maxLength": 2048, - "minLength": 0, - "type": "string", - "description": "HTTPS endpoint that receives webhook event payloads" - }, - "description": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Optional human-readable description" - }, - "subscribedEvents": { - "minItems": 1, - "type": "array", - "description": "Event types to deliver, e.g. monitor.created, incident.resolved", - "items": { - "minLength": 1, - "type": "string", - "description": "Event types to deliver, e.g. monitor.created, incident.resolved" - } - } - } - }, - "TestWebhookEndpointRequest": { - "type": "object", - "properties": { - "eventType": { - "type": "string", - "description": "Event type to simulate (e.g. monitor.created); null uses a default", - "nullable": true - } - }, - "description": "Event type to use for a test webhook delivery" - }, - "SingleValueResponseWebhookTestResult": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/WebhookTestResult" - } - } - }, - "WebhookTestResult": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "statusCode": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "message": { - "type": "string" - }, - "durationMs": { - "type": "integer", - "format": "int64", - "nullable": true - } - } - }, - "SingleValueResponseString": { - "type": "object", - "properties": { - "data": { - "type": "string", - "nullable": true - } - } - }, - "DekRotationResultDto": { - "type": "object", - "properties": { - "previousDekVersion": { - "type": "integer", - "description": "DEK version before rotation", - "format": "int32" - }, - "newDekVersion": { - "type": "integer", - "description": "DEK version after rotation", - "format": "int32" - }, - "secretsReEncrypted": { - "type": "integer", - "description": "Number of secrets re-encrypted with the new DEK", - "format": "int32" - }, - "channelsReEncrypted": { - "type": "integer", - "description": "Number of alert channels re-encrypted with the new DEK", - "format": "int32" - }, - "rotatedAt": { - "type": "string", - "description": "Timestamp when the rotation was performed", - "format": "date-time" - } - }, - "description": "Result of a data encryption key rotation operation" - }, - "SingleValueResponseDekRotationResultDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/DekRotationResultDto" - } - } - }, - "CreateTagRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "Tag name, unique within the org" - }, - "color": { - "pattern": "^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$", - "type": "string", - "description": "Hex color code (defaults to #6B7280 if omitted)", - "nullable": true - } - }, - "description": "Request body for creating a tag" - }, - "ServiceSubscribeRequest": { - "type": "object", - "properties": { - "componentId": { - "type": "string", - "description": "ID of the component to subscribe to. Omit or null for whole-service subscription.", - "format": "uuid", - "nullable": true - }, - "alertSensitivity": { - "type": "string", - "description": "Alert sensitivity level. Defaults to INCIDENTS_ONLY when not provided.", - "nullable": true - } - }, - "description": "Optional body for subscribing to a specific component of a service" - }, - "ComponentUptimeSummaryDto": { - "type": "object", - "properties": { - "day": { - "type": "number", - "description": "Uptime percentage over the last 24 hours", - "format": "double", - "nullable": true, - "example": 99.95 - }, - "week": { - "type": "number", - "description": "Uptime percentage over the last 7 days", - "format": "double", - "nullable": true, - "example": 99.98 - }, - "month": { - "type": "number", - "description": "Uptime percentage over the last 30 days", - "format": "double", - "nullable": true, - "example": 99.92 - }, - "source": { - "type": "string", - "description": "Data source: vendor_reported or incident_derived", - "example": "vendor_reported" - } - }, - "description": "Inline uptime percentages for 24h, 7d, 30d" - }, - "ServiceComponentDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "externalId": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "description": { - "type": "string", - "nullable": true - }, - "groupId": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "position": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "showcase": { - "type": "boolean" - }, - "onlyShowIfDegraded": { - "type": "boolean" - }, - "startDate": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "vendorCreatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "lifecycleStatus": { - "type": "string" - }, - "dataType": { - "type": "string", - "description": "Data classification: full, status_only, or metric_only", - "example": "full" - }, - "hasUptime": { - "type": "boolean", - "description": "Whether uptime data is available for this component" - }, - "region": { - "type": "string", - "description": "Geographic region for regional components (AWS, GCP, Azure)", - "nullable": true - }, - "groupName": { - "type": "string", - "description": "Display name of the parent group", - "nullable": true - }, - "uptime": { - "$ref": "#/components/schemas/ComponentUptimeSummaryDto" - }, - "statusChangedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "firstSeenAt": { - "type": "string", - "format": "date-time" - }, - "lastSeenAt": { - "type": "string", - "format": "date-time" - }, - "group": { - "type": "boolean" - } - }, - "description": "A first-class service component with lifecycle and uptime data" - }, - "ServiceSubscriptionDto": { - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "Unique subscription identifier", - "format": "uuid" - }, - "serviceId": { - "type": "string", - "description": "Service identifier", - "format": "uuid" - }, - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "category": { - "type": "string", - "nullable": true - }, - "officialStatusUrl": { - "type": "string", - "nullable": true - }, - "adapterType": { - "type": "string" - }, - "pollingIntervalSeconds": { - "type": "integer", - "format": "int32" - }, - "enabled": { - "type": "boolean" - }, - "logoUrl": { - "type": "string", - "description": "Logo URL from the service catalog", - "nullable": true - }, - "overallStatus": { - "type": "string", - "description": "Current overall status; null when the service has never been polled", - "nullable": true - }, - "componentId": { - "type": "string", - "description": "Subscribed component id; null for whole-service subscription", - "format": "uuid", - "nullable": true - }, - "component": { - "$ref": "#/components/schemas/ServiceComponentDto" - }, - "alertSensitivity": { - "type": "string", - "description": "Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity)", - "enum": [ - "ALL", - "INCIDENTS_ONLY", - "MAJOR_ONLY" - ] - }, - "subscribedAt": { - "type": "string", - "description": "When the organization subscribed to this service", - "format": "date-time" - } - }, - "description": "An org-level service subscription with current status information" - }, - "SingleValueResponseServiceSubscriptionDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ServiceSubscriptionDto" - } - } - }, - "CreateSecretRequest": { - "required": [ - "key", - "value" - ], - "type": "object", - "properties": { - "key": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Unique secret key within the workspace (max 255 chars)" - }, - "value": { - "maxLength": 32768, - "minLength": 0, - "type": "string", - "description": "Secret value, stored encrypted (max 32KB)" - } - } - }, - "CreateResourceGroupRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this group" - }, - "description": { - "type": "string", - "description": "Optional description", - "nullable": true - }, - "alertPolicyId": { - "type": "string", - "description": "Optional notification policy to apply for this group", - "format": "uuid", - "nullable": true - }, - "defaultFrequency": { - "maximum": 86400, - "minimum": 30, - "type": "integer", - "description": "Default check frequency in seconds applied to members (30\u201386400)", - "format": "int32", - "nullable": true - }, - "defaultRegions": { - "type": "array", - "description": "Default regions applied to member monitors", - "nullable": true, - "items": { - "type": "string", - "description": "Default regions applied to member monitors", - "nullable": true - } - }, - "defaultRetryStrategy": { - "$ref": "#/components/schemas/RetryStrategy" - }, - "defaultAlertChannels": { - "type": "array", - "description": "Default alert channel IDs applied to member monitors", - "nullable": true, - "items": { - "type": "string", - "description": "Default alert channel IDs applied to member monitors", - "format": "uuid", - "nullable": true - } - }, - "defaultEnvironmentId": { - "type": "string", - "description": "Default environment ID applied to member monitors", - "format": "uuid", - "nullable": true - }, - "healthThresholdType": { - "type": "string", - "description": "Health threshold type: COUNT or PERCENTAGE", - "nullable": true, - "enum": [ - "COUNT", - "PERCENTAGE" - ] - }, - "healthThresholdValue": { - "maximum": 100, - "exclusiveMaximum": false, - "minimum": 0, - "exclusiveMinimum": false, - "type": "number", - "description": "Health threshold value: count (0+) or percentage (0\u2013100)", - "nullable": true - }, - "suppressMemberAlerts": { - "type": "boolean", - "description": "Suppress member-level alert notifications when group manages alerting", - "nullable": true - }, - "confirmationDelaySeconds": { - "maximum": 600, - "minimum": 0, - "type": "integer", - "description": "Confirmation delay in seconds before group incident creation (0\u2013600)", - "format": "int32", - "nullable": true - }, - "recoveryCooldownMinutes": { - "maximum": 60, - "minimum": 0, - "type": "integer", - "description": "Recovery cooldown in minutes after group incident resolves (0\u201360)", - "format": "int32", - "nullable": true - } - }, - "description": "Request body for creating a resource group" - }, - "AddResourceGroupMemberRequest": { - "required": [ - "memberId", - "memberType" - ], - "type": "object", - "properties": { - "memberType": { - "minLength": 1, - "pattern": "monitor|service", - "type": "string", - "description": "Type of member: 'monitor' or 'service'" - }, - "memberId": { - "type": "string", - "description": "ID of the monitor or service to add", - "format": "uuid" - } - }, - "description": "Request body for adding a member to a resource group" - }, - "SingleValueResponseResourceGroupMemberDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ResourceGroupMemberDto" - } - } - }, - "CreateNotificationPolicyRequest": { - "required": [ - "escalation", - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this policy" - }, - "matchRules": { - "type": "array", - "description": "Match rules to evaluate (all must pass; omit or empty for catch-all)", - "items": { - "$ref": "#/components/schemas/MatchRule" - } - }, - "escalation": { - "$ref": "#/components/schemas/EscalationChain" - }, - "enabled": { - "type": "boolean", - "description": "Whether this policy is enabled (default true)", - "default": true - }, - "priority": { - "type": "integer", - "description": "Evaluation priority; higher value = evaluated first (default 0)", - "format": "int32", - "default": 0 - } - }, - "description": "Request body for creating a notification policy" - }, - "TestNotificationPolicyRequest": { - "type": "object", - "properties": { - "severity": { - "type": "string", - "description": "Incident severity to test against (e.g. DOWN, DEGRADED, MAINTENANCE)", - "nullable": true - }, - "monitorId": { - "type": "string", - "description": "Monitor UUID to test against (monitoring events)", - "format": "uuid", - "nullable": true - }, - "regions": { - "type": "array", - "description": "Affected region identifiers to test against (monitoring events)", - "nullable": true, - "items": { - "type": "string", - "description": "Affected region identifiers to test against (monitoring events)", - "nullable": true - } - }, - "eventType": { - "type": "string", - "description": "Incident event type to test against \u2014 short form (e.g. created, resolved, reopened) or full form (e.g. incident.created)", - "nullable": true - }, - "monitorType": { - "type": "string", - "description": "Monitor check type to test against (e.g. HTTP, DNS, MCP_SERVER)", - "nullable": true - }, - "serviceId": { - "type": "string", - "description": "Service catalog UUID to test against (status data events)", - "format": "uuid", - "nullable": true - }, - "componentName": { - "type": "string", - "description": "Component name to test against (status data events, e.g. \"Actions\")", - "nullable": true - }, - "resourceGroupIds": { - "type": "array", - "description": "Resource group UUIDs the entity belongs to, for resource_group_id_in rules", - "nullable": true, - "items": { - "type": "string", - "description": "Resource group UUIDs the entity belongs to, for resource_group_id_in rules", - "format": "uuid", - "nullable": true - } - } - }, - "description": "Event context for a dry-run match evaluation against a notification policy" - }, - "SingleValueResponseTestMatchResult": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/TestMatchResult" - } - } - }, - "TestMatchResult": { - "type": "object", - "properties": { - "matched": { - "type": "boolean", - "description": "Whether the policy would match the supplied incident context" - }, - "matchedRules": { - "type": "array", - "description": "Rules that passed evaluation", - "items": { - "type": "string", - "description": "Rules that passed evaluation" - } - }, - "unmatchedRules": { - "type": "array", - "description": "Rules that did not pass evaluation", - "items": { - "type": "string", - "description": "Rules that did not pass evaluation" - } - } - }, - "description": "Result of a dry-run match evaluation against a notification policy" - }, - "AlertDeliveryDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "incidentId": { - "type": "string", - "description": "Incident that triggered this delivery", - "format": "uuid" - }, - "dispatchId": { - "type": "string", - "description": "Notification dispatch that created this delivery", - "format": "uuid", - "nullable": true - }, - "channelId": { - "type": "string", - "description": "Alert channel ID", - "format": "uuid" - }, - "channel": { - "type": "string", - "description": "Human-readable channel name" - }, - "channelType": { - "type": "string", - "description": "Alert channel type (e.g. slack, email, webhook)" - }, - "status": { - "type": "string", - "description": "Current delivery status", - "enum": [ - "PENDING", - "DELIVERED", - "RETRY_PENDING", - "FAILED", - "CANCELLED" - ] - }, - "eventType": { - "type": "string", - "description": "Incident lifecycle event that triggered this delivery", - "enum": [ - "INCIDENT_CREATED", - "INCIDENT_RESOLVED", - "INCIDENT_REOPENED" - ] - }, - "stepNumber": { - "type": "integer", - "description": "1-based escalation step this delivery belongs to", - "format": "int32" - }, - "fireCount": { - "type": "integer", - "description": "Fire sequence within the step: 1 = initial, 2+ = repeat re-fires", - "format": "int32" - }, - "attemptCount": { - "type": "integer", - "description": "Number of delivery attempts made", - "format": "int32" - }, - "lastAttemptAt": { - "type": "string", - "description": "When the last attempt was made", - "format": "date-time", - "nullable": true - }, - "nextRetryAt": { - "type": "string", - "description": "When the next retry is scheduled (null if not retrying)", - "format": "date-time", - "nullable": true - }, - "deliveredAt": { - "type": "string", - "description": "Timestamp when the delivery was confirmed (null if not yet delivered)", - "format": "date-time", - "nullable": true - }, - "errorMessage": { - "type": "string", - "description": "Error message from the last failed attempt", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "description": "Delivery record for a single channel within a notification dispatch" - }, - "NotificationDispatchDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique dispatch record identifier", - "format": "uuid" - }, - "incidentId": { - "type": "string", - "description": "Incident this dispatch is for", - "format": "uuid" - }, - "policyId": { - "type": "string", - "description": "Notification policy that matched this incident", - "format": "uuid" - }, - "policyName": { - "type": "string", - "description": "Human-readable name of the matched policy (null if policy has been deleted)", - "nullable": true - }, - "status": { - "type": "string", - "description": "Current dispatch state", - "enum": [ - "PENDING", - "DISPATCHING", - "DELIVERED", - "ESCALATING", - "ACKNOWLEDGED", - "COMPLETED" - ] - }, - "completionReason": { - "type": "string", - "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.", - "nullable": true, - "enum": [ - "EXHAUSTED", - "RESOLVED", - "NO_STEPS" - ] - }, - "currentStep": { - "type": "integer", - "description": "1-based index of the currently active escalation step", - "format": "int32" - }, - "totalSteps": { - "type": "integer", - "description": "Total number of escalation steps in the policy (null if policy has been deleted)", - "format": "int32", - "nullable": true - }, - "acknowledgedAt": { - "type": "string", - "description": "Timestamp when this dispatch was acknowledged (null if not acknowledged)", - "format": "date-time", - "nullable": true - }, - "nextEscalationAt": { - "type": "string", - "description": "Timestamp when the next escalation step will fire (null if not scheduled)", - "format": "date-time", - "nullable": true - }, - "lastNotifiedAt": { - "type": "string", - "description": "Timestamp of the most recent notification delivery", - "format": "date-time", - "nullable": true - }, - "deliveries": { - "type": "array", - "description": "Delivery records for all channels associated with this dispatch", - "items": { - "$ref": "#/components/schemas/AlertDeliveryDto" - } - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the dispatch was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the dispatch was last updated", - "format": "date-time" - } - }, - "description": "Dispatch state for a single (incident, notification policy) pair, with delivery history" - }, - "SingleValueResponseNotificationDispatchDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/NotificationDispatchDto" - } - } - }, - "CreateMonitorRequest": { - "required": [ - "config", - "managedBy", - "name", - "type" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this monitor" - }, - "type": { - "type": "string", - "description": "Monitor protocol type", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] - }, - "frequencySeconds": { - "type": "integer", - "description": "Check frequency in seconds (30\u201386400, default: 60)", - "format": "int32" - }, - "enabled": { - "type": "boolean", - "description": "Whether the monitor is active (default: true)", - "nullable": true - }, - "regions": { - "type": "array", - "description": "Probe regions to run checks from, e.g. us-east, eu-west", - "nullable": true, - "items": { - "type": "string", - "description": "Probe regions to run checks from, e.g. us-east, eu-west", - "nullable": true - } - }, - "managedBy": { - "type": "string", - "description": "Who manages this monitor: DASHBOARD or CLI", - "enum": [ - "DASHBOARD", - "CLI" - ] - }, - "environmentId": { - "type": "string", - "description": "Environment to associate with this monitor", - "format": "uuid", - "nullable": true - }, - "assertions": { - "type": "array", - "description": "Assertions to evaluate against each check result", - "nullable": true, - "items": { - "$ref": "#/components/schemas/CreateAssertionRequest" - } - }, - "auth": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - }, - "incidentPolicy": { - "$ref": "#/components/schemas/UpdateIncidentPolicyRequest" - }, - "alertChannelIds": { - "type": "array", - "description": "Alert channels to notify when this monitor triggers", - "nullable": true, - "items": { - "type": "string", - "description": "Alert channels to notify when this monitor triggers", - "format": "uuid", - "nullable": true - } - }, - "tags": { - "$ref": "#/components/schemas/AddMonitorTagsRequest" - } - } - }, - "SetMonitorAuthRequest": { - "required": [ - "config" - ], - "type": "object", - "properties": { - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/ApiKeyAuthConfig" - }, - { - "$ref": "#/components/schemas/BasicAuthConfig" - }, - { - "$ref": "#/components/schemas/BearerAuthConfig" - }, - { - "$ref": "#/components/schemas/HeaderAuthConfig" - } - ] - } - } - }, - "AssertionTestResultDto": { - "type": "object", - "properties": { - "assertionType": { - "type": "string", - "description": "Assertion type evaluated", - "enum": [ - "status_code", - "response_time", - "body_contains", - "json_path", - "header", - "regex", - "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" - ] - }, - "passed": { - "type": "boolean", - "description": "Whether the assertion passed" - }, - "severity": { - "type": "string", - "description": "Assertion severity: FAIL or WARN", - "enum": [ - "fail", - "warn" - ] - }, - "message": { - "type": "string", - "description": "Human-readable result description" - }, - "expected": { - "type": "string", - "description": "Expected value", - "nullable": true - }, - "actual": { - "type": "string", - "description": "Actual value observed during the test", - "nullable": true - } - } - }, - "MonitorTestResultDto": { - "type": "object", - "properties": { - "passed": { - "type": "boolean" - }, - "error": { - "type": "string", - "nullable": true - }, - "statusCode": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "responseTimeMs": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - }, - "nullable": true - }, - "bodyPreview": { - "type": "string", - "nullable": true - }, - "responseSizeBytes": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "redirectCount": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "finalUrl": { - "type": "string", - "nullable": true - }, - "assertionResults": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AssertionTestResultDto" - } - }, - "warnings": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - } - } - }, - "SingleValueResponseMonitorTestResultDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MonitorTestResultDto" - } - } - }, - "TableValueResultTagDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TagDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "MonitorTestRequest": { - "required": [ - "config", - "type" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Monitor protocol type to test", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] - }, - "assertions": { - "type": "array", - "description": "Optional assertions to evaluate against the test result", - "nullable": true, - "items": { - "$ref": "#/components/schemas/CreateAssertionRequest" - } - } - } - }, - "BulkMonitorActionRequest": { - "required": [ - "action", - "monitorIds" - ], - "type": "object", - "properties": { - "monitorIds": { - "maxItems": 200, - "minItems": 0, - "type": "array", - "description": "IDs of monitors to act on (max 200)", - "items": { - "type": "string", - "description": "IDs of monitors to act on (max 200)", - "format": "uuid" - } - }, - "action": { - "type": "string", - "description": "Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG", - "enum": [ - "PAUSE", - "RESUME", - "DELETE", - "ADD_TAG", - "REMOVE_TAG" - ] - }, - "tagIds": { - "type": "array", - "description": "Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)", - "nullable": true, - "items": { - "type": "string", - "description": "Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)", - "format": "uuid", - "nullable": true - } - }, - "newTags": { - "type": "array", - "description": "New tags to create and attach (only for ADD_TAG)", - "nullable": true, - "items": { - "$ref": "#/components/schemas/NewTagRequest" - } - } - }, - "description": "Request body for performing a bulk action on multiple monitors" - }, - "BulkMonitorActionResult": { - "type": "object", - "properties": { - "succeeded": { - "type": "array", - "description": "IDs of monitors on which the action succeeded", - "items": { - "type": "string", - "description": "IDs of monitors on which the action succeeded", - "format": "uuid" - } - }, - "failed": { - "type": "array", - "description": "Monitors on which the action failed, with the reason for each failure", - "items": { - "$ref": "#/components/schemas/FailureDetail" - } - } - }, - "description": "Result of a bulk monitor action, including partial-success details" - }, - "FailureDetail": { - "type": "object", - "properties": { - "monitorId": { - "type": "string", - "description": "Monitor ID that failed", - "format": "uuid" - }, - "reason": { - "type": "string", - "description": "Human-readable reason for the failure" - } - }, - "description": "Details about a single monitor that failed the bulk action" - }, - "SingleValueResponseBulkMonitorActionResult": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/BulkMonitorActionResult" - } - } - }, - "CreateMaintenanceWindowRequest": { - "required": [ - "endsAt", - "startsAt" - ], - "type": "object", - "properties": { - "monitorId": { - "type": "string", - "description": "Monitor to attach this maintenance window to; null for org-wide", - "format": "uuid" - }, - "startsAt": { - "type": "string", - "description": "Scheduled start of the maintenance window (ISO 8601)", - "format": "date-time" - }, - "endsAt": { - "type": "string", - "description": "Scheduled end of the maintenance window (ISO 8601)", - "format": "date-time" - }, - "repeatRule": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "iCal RRULE for recurring windows (max 100 chars); null for one-time" - }, - "reason": { - "type": "string", - "description": "Human-readable reason for the maintenance" - }, - "suppressAlerts": { - "type": "boolean", - "description": "Whether to suppress alerts during this window (default: true)" - } - } - }, - "CreateInviteRequest": { - "required": [ - "email", - "roleOffered" - ], - "type": "object", - "properties": { - "email": { - "minLength": 1, - "type": "string", - "description": "Email address to invite", - "format": "email" - }, - "roleOffered": { - "type": "string", - "description": "Role to assign on acceptance", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - } - }, - "description": "Invite a new member to the organization by email" - }, - "InviteDto": { - "type": "object", - "properties": { - "inviteId": { - "type": "integer", - "description": "Unique invite identifier", - "format": "int32" - }, - "email": { - "type": "string", - "description": "Email address the invite was sent to" - }, - "roleOffered": { - "type": "string", - "description": "Role that will be assigned to the invitee on acceptance", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - }, - "expiresAt": { - "type": "string", - "description": "Timestamp when the invite expires", - "format": "date-time" - }, - "consumedAt": { - "type": "string", - "description": "Timestamp when the invite was accepted; null if not yet used", - "format": "date-time", - "nullable": true - }, - "revokedAt": { - "type": "string", - "description": "Timestamp when the invite was revoked; null if active", - "format": "date-time", - "nullable": true - } - }, - "description": "Organization invite sent to an email address" - }, - "SingleValueResponseInviteDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/InviteDto" - } - } - }, - "CreateManualIncidentRequest": { - "required": [ - "severity", - "title" - ], - "type": "object", - "properties": { - "title": { - "minLength": 1, - "type": "string", - "description": "Short summary of the incident" - }, - "severity": { - "type": "string", - "description": "Incident severity: DOWN, DEGRADED, or MAINTENANCE", - "enum": [ - "DOWN", - "DEGRADED", - "MAINTENANCE" - ] - }, - "monitorId": { - "type": "string", - "description": "Monitor to associate with this incident", - "format": "uuid", - "nullable": true - }, - "body": { - "type": "string", - "description": "Detailed description or context for the incident", - "nullable": true - } - } - }, - "IncidentDetailDto": { - "type": "object", - "properties": { - "incident": { - "$ref": "#/components/schemas/IncidentDto" - }, - "updates": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncidentUpdateDto" - } - } - } - }, - "IncidentUpdateDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "incidentId": { - "type": "string", - "format": "uuid" - }, - "oldStatus": { - "type": "string", - "nullable": true, - "enum": [ - "WATCHING", - "TRIGGERED", - "CONFIRMED", - "RESOLVED" - ] - }, - "newStatus": { - "type": "string", - "nullable": true, - "enum": [ - "WATCHING", - "TRIGGERED", - "CONFIRMED", - "RESOLVED" - ] - }, - "body": { - "type": "string", - "nullable": true - }, - "createdBy": { - "type": "string", - "enum": [ - "SYSTEM", - "USER" - ] - }, - "notifySubscribers": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "SingleValueResponseIncidentDetailDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/IncidentDetailDto" - } - } - }, - "AddIncidentUpdateRequest": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "Update message or post-mortem notes" - }, - "newStatus": { - "type": "string", - "description": "Updated incident status; null to keep current status", - "enum": [ - "WATCHING", - "TRIGGERED", - "CONFIRMED", - "RESOLVED" - ] - }, - "notifySubscribers": { - "type": "boolean", - "description": "Whether to notify subscribers of this update" - } - } - }, - "ResolveIncidentRequest": { - "type": "object", - "properties": { - "body": { - "type": "string", - "description": "Optional resolution message or post-mortem notes" - } - } - }, - "CreateEnvironmentRequest": { - "required": [ - "name", - "slug" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 100, - "minLength": 0, - "type": "string", - "description": "Human-readable environment name" - }, - "slug": { - "maxLength": 100, - "minLength": 0, - "pattern": "^[a-z0-9][a-z0-9_-]*$", - "type": "string", - "description": "URL-safe identifier (lowercase alphanumeric, hyphens, underscores)" - }, - "variables": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "Initial key-value variable pairs for this environment", - "nullable": true - }, - "description": "Initial key-value variable pairs for this environment", - "nullable": true - }, - "isDefault": { - "type": "boolean", - "description": "Whether this is the default environment for new monitors" - } - } - }, - "AcquireDeployLockRequest": { - "required": [ - "lockedBy" - ], - "type": "object", - "properties": { - "lockedBy": { - "minLength": 1, - "type": "string", - "description": "Identity of the lock requester (e.g. hostname, CI job ID)" - }, - "ttlMinutes": { - "type": "integer", - "description": "Lock TTL in minutes (default: 30, max: 60)", - "format": "int32", - "nullable": true, - "example": 30 - } - }, - "description": "Request to acquire a deploy lock for the current workspace" - }, - "DeployLockDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique lock identifier", - "format": "uuid" - }, - "lockedBy": { - "type": "string", - "description": "Identity of the lock holder (e.g. CLI session ID, username)" - }, - "lockedAt": { - "type": "string", - "description": "Timestamp when the lock was acquired", - "format": "date-time" - }, - "expiresAt": { - "type": "string", - "description": "Timestamp when the lock automatically expires", - "format": "date-time" - } - }, - "description": "Represents an active deploy lock for a workspace" - }, - "SingleValueResponseDeployLockDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/DeployLockDto" - } - } - }, - "CreateApiKeyRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "Human-readable name to identify this API key" - }, - "expiresAt": { - "type": "string", - "description": "Optional expiration timestamp in ISO 8601 format", - "format": "date-time", - "nullable": true - } - } - }, - "ApiKeyCreateResponse": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique API key identifier", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable name for this API key" - }, - "key": { - "type": "string", - "description": "Full API key value in dh_live_* format; store this now" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the key was created", - "format": "date-time" - }, - "expiresAt": { - "type": "string", - "description": "Timestamp when the key expires; null if no expiration", - "format": "date-time", - "nullable": true - } - }, - "description": "Created API key with the full key value \u2014 store it now, it won't be shown again" - }, - "SingleValueResponseApiKeyCreateResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiKeyCreateResponse" - } - } - }, - "ApiKeyDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique API key identifier", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable name for this API key" - }, - "key": { - "type": "string", - "description": "Full API key value in dh_live_* format" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the key was created", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "Timestamp when the key was last updated", - "format": "date-time" - }, - "lastUsedAt": { - "type": "string", - "description": "Timestamp of the most recent API call; null if never used", - "format": "date-time", - "nullable": true - }, - "revokedAt": { - "type": "string", - "description": "Timestamp when the key was revoked; null if active", - "format": "date-time", - "nullable": true - }, - "expiresAt": { - "type": "string", - "description": "Timestamp when the key expires; null if no expiration", - "format": "date-time", - "nullable": true - } - }, - "description": "API key for programmatic access to the DevHelm API" - }, - "SingleValueResponseApiKeyDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ApiKeyDto" - } - } - }, - "SingleValueResponseAlertDeliveryDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AlertDeliveryDto" - } - } - }, - "CreateAlertChannelRequest": { - "required": [ - "config", - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 255, - "minLength": 0, - "type": "string", - "description": "Human-readable name for this alert channel" - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DiscordChannelConfig" - }, - { - "$ref": "#/components/schemas/EmailChannelConfig" - }, - { - "$ref": "#/components/schemas/OpsGenieChannelConfig" - }, - { - "$ref": "#/components/schemas/PagerDutyChannelConfig" - }, - { - "$ref": "#/components/schemas/SlackChannelConfig" - }, - { - "$ref": "#/components/schemas/TeamsChannelConfig" - }, - { - "$ref": "#/components/schemas/WebhookChannelConfig" - } - ] - } - } - }, - "SingleValueResponseTestChannelResult": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/TestChannelResult" - } - } - }, - "TestChannelResult": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - } - } - }, - "TestAlertChannelRequest": { - "required": [ - "config" - ], - "type": "object", - "properties": { - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DiscordChannelConfig" - }, - { - "$ref": "#/components/schemas/EmailChannelConfig" - }, - { - "$ref": "#/components/schemas/OpsGenieChannelConfig" - }, - { - "$ref": "#/components/schemas/PagerDutyChannelConfig" - }, - { - "$ref": "#/components/schemas/SlackChannelConfig" - }, - { - "$ref": "#/components/schemas/TeamsChannelConfig" - }, - { - "$ref": "#/components/schemas/WebhookChannelConfig" - } - ] - } - }, - "description": "Alert channel configuration to test without saving" - }, - "ComponentUpdateRequest": { - "required": [ - "addComponents" - ], - "type": "object", - "properties": { - "addComponents": { - "minItems": 1, - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "UpdateAlertSensitivityRequest": { - "required": [ - "alertSensitivity" - ], - "type": "object", - "properties": { - "alertSensitivity": { - "minLength": 1, - "pattern": "ALL|INCIDENTS_ONLY|MAJOR_ONLY", - "type": "string", - "description": "Alert sensitivity: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents)" - } - }, - "description": "Request body for updating alert sensitivity on a service subscription" - }, - "UpdateApiKeyRequest": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "maxLength": 200, - "minLength": 0, - "type": "string", - "description": "New name for this API key" - } - } - }, - "TableValueResultWorkspaceDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WorkspaceDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseMapStringString": { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": { - "type": "string", - "nullable": true - }, - "nullable": true - } - } - }, - "SingleValueResponseListMonitorAssertionDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "nullable": true, - "items": { - "$ref": "#/components/schemas/MonitorAssertionDto" - } - } - } - }, - "SchedulableMonitorDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique monitor identifier", - "format": "uuid" - }, - "type": { - "type": "string", - "description": "Monitor protocol type", - "enum": [ - "HTTP", - "DNS", - "MCP_SERVER", - "TCP", - "ICMP", - "HEARTBEAT" - ] - }, - "config": { - "oneOf": [ - { - "$ref": "#/components/schemas/DnsMonitorConfig" - }, - { - "$ref": "#/components/schemas/HeartbeatMonitorConfig" - }, - { - "$ref": "#/components/schemas/HttpMonitorConfig" - }, - { - "$ref": "#/components/schemas/IcmpMonitorConfig" - }, - { - "$ref": "#/components/schemas/McpServerMonitorConfig" - }, - { - "$ref": "#/components/schemas/TcpMonitorConfig" - } - ] - }, - "frequencySeconds": { - "type": "integer", - "description": "Check frequency in seconds", - "format": "int32" - }, - "regions": { - "type": "array", - "description": "Probe regions to execute checks from", - "items": { - "type": "string", - "description": "Probe regions to execute checks from" - } - }, - "organizationId": { - "type": "integer", - "description": "Organization this monitor belongs to", - "format": "int32" - } - } - }, - "TableValueResultAdapterHealthDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AdapterHealthDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseListBillingPlanDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "nullable": true, - "items": { - "$ref": "#/components/schemas/BillingPlanDto" - } - } - } - }, - "TableValueResultTransactionDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TransactionDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultSubscriptionDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SubscriptionDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseUpcomingChargeResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/UpcomingChargeResponse" - } - } - }, - "UpcomingChargeResponse": { - "type": "object", - "properties": { - "action": { - "type": "string", - "description": "Type of subscription action being previewed", - "enum": [ - "UPGRADE", - "DOWNGRADE", - "NOOP" - ] - }, - "immediateAmount": { - "type": "integer", - "description": "Amount due immediately (proration) in smallest currency unit", - "format": "int32" - }, - "nextBillingAmount": { - "type": "integer", - "description": "Amount that will be charged on the next billing cycle", - "format": "int32" - }, - "nextBillingDate": { - "type": "string", - "description": "Date of the next billing cycle; null if cancelling", - "format": "date-time", - "nullable": true - } - }, - "description": "Preview of upcoming subscription charge after a plan change" - }, - "EntitlementDto": { - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "Entitlement key" - }, - "value": { - "type": "integer", - "description": "Effective limit value (overrides applied)", - "format": "int64" - }, - "defaultValue": { - "type": "integer", - "description": "Plan-tier default value before overrides", - "format": "int64" - }, - "overridden": { - "type": "boolean", - "description": "Whether this entitlement has an org-level override" - } - }, - "description": "A single resolved entitlement for the organization" - }, - "EntitlementResponse": { - "type": "object", - "properties": { - "tier": { - "type": "string", - "description": "Resolved billing plan tier", - "enum": [ - "FREE", - "STARTER", - "PRO", - "TEAM", - "BUSINESS", - "ENTERPRISE" - ] - }, - "entitlements": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/EntitlementDto" - }, - "description": "All entitlements keyed by entitlement key" - }, - "usage": { - "type": "object", - "additionalProperties": { - "type": "integer", - "description": "Current usage counters keyed by entitlement key (only for countable resources)", - "format": "int64" - }, - "description": "Current usage counters keyed by entitlement key (only for countable resources)" - }, - "trialActive": { - "type": "boolean", - "description": "Whether the org is currently on a trial" - }, - "trialExpiresAt": { - "type": "string", - "description": "Trial expiry date (null if not trialing)", - "format": "date-time", - "nullable": true - }, - "subscriptionStatus": { - "type": "string", - "description": "Current subscription status (null if no subscription)", - "nullable": true - } - }, - "description": "Full entitlement state for an organization: resolved limits, usage, and trial info" - }, - "SingleValueResponseEntitlementResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/EntitlementResponse" - } - } - }, - "PaginationParams": { - "required": [ - "sortBy", - "sortOrder" - ], - "type": "object", - "properties": { - "sortBy": { - "type": "string" - }, - "sortOrder": { - "type": "string", - "enum": [ - "ASC", - "DESC" - ] - }, - "page": { - "minimum": 0, - "type": "integer", - "format": "int32" - }, - "size": { - "maximum": 200, - "minimum": 1, - "type": "integer", - "format": "int32" - } - } - }, - "IdValuePair": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Numeric identifier", - "format": "int32" - }, - "value": { - "type": "string", - "description": "Display label or value" - } - }, - "description": "Generic id/value pair for select options and autocomplete" - }, - "TableValueResultIdValuePair": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IdValuePair" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "MyOrgItemDto": { - "type": "object", - "properties": { - "orgId": { - "type": "integer", - "description": "Organization identifier", - "format": "int32" - }, - "orgName": { - "type": "string", - "description": "Organization name" - }, - "orgRole": { - "type": "string", - "description": "Member role within this organization", - "enum": [ - "OWNER", - "ADMIN", - "MEMBER" - ] - }, - "status": { - "type": "string", - "description": "Membership status", - "enum": [ - "INVITED", - "ACTIVE", - "SUSPENDED", - "LEFT", - "REMOVED", - "DECLINED" - ] - } - }, - "description": "Membership summary for an organization the user belongs to" - }, - "TableValueResultMyOrgItemDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MyOrgItemDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SseEmitter": { - "type": "object", - "properties": { - "timeout": { - "type": "integer", - "format": "int64", - "nullable": true - } - } - }, - "Pageable": { - "type": "object", - "properties": { - "page": { - "minimum": 0, - "type": "integer", - "format": "int32" - }, - "size": { - "minimum": 1, - "type": "integer", - "format": "int32" - }, - "sort": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "TableValueResultUserDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "AdminStatsDto": { - "type": "object", - "properties": { - "userCount": { - "type": "integer", - "format": "int64" - }, - "orgCount": { - "type": "integer", - "format": "int64" - }, - "memberCount": { - "type": "integer", - "format": "int64" - } - } - }, - "SingleValueResponseAdminStatsDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AdminStatsDto" - } - } - }, - "TableValueResultOrganizationDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizationDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultMemberDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemberDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultWebhookEndpointDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookEndpointDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultWebhookDeliveryDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookDeliveryDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "WebhookDeliveryDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "endpointId": { - "type": "string", - "format": "uuid" - }, - "eventId": { - "type": "string" - }, - "eventType": { - "type": "string" - }, - "status": { - "type": "string" - }, - "attemptCount": { - "type": "integer", - "format": "int32" - }, - "maxAttempts": { - "type": "integer", - "format": "int32" - }, - "responseStatus": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "responseLatencyMs": { - "type": "integer", - "format": "int32", - "nullable": true - }, - "errorMessage": { - "type": "string", - "nullable": true - }, - "deliveredAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "failedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "nextRetryAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - } - }, - "SingleValueResponseWebhookSigningSecretDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/WebhookSigningSecretDto" - } - } - }, - "WebhookSigningSecretDto": { - "type": "object", - "properties": { - "configured": { - "type": "boolean" - }, - "maskedSecret": { - "type": "string", - "nullable": true - } - } - }, - "WebhookEventCatalogEntry": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Dot-notation event type identifier, e.g. \"monitor.created\"" - }, - "surface": { - "type": "string", - "description": "Product surface this event belongs to, e.g. \"monitoring\" or \"status_data\"" - }, - "description": { - "type": "string", - "description": "Human-readable description of when this event fires" - } - }, - "description": "List of all available webhook event types" - }, - "WebhookEventCatalogResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "description": "List of all available webhook event types", - "items": { - "$ref": "#/components/schemas/WebhookEventCatalogEntry" - } - } - } - }, - "CursorPageServiceCatalogDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "description": "Items on this page", - "items": { - "$ref": "#/components/schemas/ServiceCatalogDto" - } - }, - "nextCursor": { - "type": "string", - "description": "Opaque cursor for the next page; null when there are no more results", - "nullable": true - }, - "hasMore": { - "type": "boolean", - "description": "Whether more results exist beyond this page" - } - }, - "description": "Cursor-paginated response for time-series and append-only data" - }, - "ServiceCatalogDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "category": { - "type": "string", - "nullable": true - }, - "officialStatusUrl": { - "type": "string", - "nullable": true - }, - "developerContext": { - "type": "string", - "nullable": true - }, - "logoUrl": { - "type": "string", - "nullable": true - }, - "adapterType": { - "type": "string" - }, - "pollingIntervalSeconds": { - "type": "integer", - "format": "int32" - }, - "enabled": { - "type": "boolean" - }, - "overallStatus": { - "type": "string", - "nullable": true - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "componentCount": { - "type": "integer", - "format": "int64" - }, - "activeIncidentCount": { - "type": "integer", - "format": "int64" - }, - "dataCompleteness": { - "type": "string" - } - }, - "description": "Items on this page" - }, - "MaintenanceComponentRef": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Component identifier", - "format": "uuid" - }, - "name": { - "type": "string", - "description": "Component name" - }, - "status": { - "type": "string", - "description": "Component status at the time of the maintenance update" - } - }, - "description": "A component affected by a scheduled maintenance window" - }, - "MaintenanceUpdateDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique update identifier", - "format": "uuid" - }, - "status": { - "type": "string", - "description": "Status at the time of this update" - }, - "body": { - "type": "string", - "description": "Update message from the vendor", - "nullable": true - }, - "displayAt": { - "type": "string", - "description": "Timestamp when this update was posted", - "format": "date-time", - "nullable": true - } - }, - "description": "A status update within a scheduled maintenance lifecycle" - }, - "ScheduledMaintenanceDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique maintenance record identifier", - "format": "uuid" - }, - "externalId": { - "type": "string", - "description": "Vendor-assigned maintenance identifier" - }, - "title": { - "type": "string", - "description": "Maintenance title as reported by the vendor" - }, - "status": { - "type": "string", - "description": "Current maintenance status (scheduled, in_progress, completed)" - }, - "impact": { - "type": "string", - "description": "Reported impact level", - "nullable": true - }, - "shortlink": { - "type": "string", - "description": "Vendor-provided short URL to the maintenance page", - "nullable": true - }, - "scheduledFor": { - "type": "string", - "description": "Timestamp when the maintenance is scheduled to begin", - "format": "date-time", - "nullable": true - }, - "scheduledUntil": { - "type": "string", - "description": "Timestamp when the maintenance is scheduled to end", - "format": "date-time", - "nullable": true - }, - "startedAt": { - "type": "string", - "description": "Timestamp when the maintenance actually started", - "format": "date-time", - "nullable": true - }, - "completedAt": { - "type": "string", - "description": "Timestamp when the maintenance was completed", - "format": "date-time", - "nullable": true - }, - "affectedComponents": { - "type": "array", - "description": "Components affected by this maintenance", - "items": { - "$ref": "#/components/schemas/MaintenanceComponentRef" - } - }, - "updates": { - "type": "array", - "description": "Status updates posted during the maintenance lifecycle", - "items": { - "$ref": "#/components/schemas/MaintenanceUpdateDto" - } - } - }, - "description": "A scheduled maintenance window from a vendor status page" - }, - "ServiceDetailDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "slug": { - "type": "string" - }, - "name": { - "type": "string" - }, - "category": { - "type": "string", - "nullable": true - }, - "officialStatusUrl": { - "type": "string", - "nullable": true - }, - "developerContext": { - "type": "string", - "nullable": true - }, - "logoUrl": { - "type": "string", - "nullable": true - }, - "adapterType": { - "type": "string" - }, - "pollingIntervalSeconds": { - "type": "integer", - "format": "int32" - }, - "enabled": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "currentStatus": { - "$ref": "#/components/schemas/ServiceStatusDto" - }, - "recentIncidents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceIncidentDto" - } - }, - "components": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceComponentDto" - } - }, - "uptime": { - "$ref": "#/components/schemas/ComponentUptimeSummaryDto" - }, - "activeMaintenances": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduledMaintenanceDto" - } - }, - "dataCompleteness": { - "type": "string" - } - } - }, - "ServiceIncidentDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "serviceId": { - "type": "string", - "format": "uuid" - }, - "serviceSlug": { - "type": "string", - "nullable": true - }, - "serviceName": { - "type": "string", - "nullable": true - }, - "externalId": { - "type": "string", - "nullable": true - }, - "title": { - "type": "string" - }, - "status": { - "type": "string" - }, - "impact": { - "type": "string", - "nullable": true - }, - "startedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "resolvedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "shortlink": { - "type": "string", - "nullable": true - }, - "detectedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "vendorCreatedAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "ServiceStatusDto": { - "type": "object", - "properties": { - "overallStatus": { - "type": "string" - }, - "lastPolledAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "SingleValueResponseServiceDetailDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ServiceDetailDto" - } - } - }, - "ServiceUptimeResponse": { - "type": "object", - "properties": { - "overallUptimePct": { - "type": "number", - "description": "Overall uptime percentage across the entire period; null when no polling data exists", - "format": "double", - "nullable": true, - "example": 99.95 - }, - "period": { - "type": "string", - "description": "Requested period", - "example": "7d" - }, - "granularity": { - "type": "string", - "description": "Requested granularity", - "example": "hourly" - }, - "buckets": { - "type": "array", - "description": "Per-bucket breakdown ordered by time ascending", - "items": { - "$ref": "#/components/schemas/UptimeBucketDto" - } - }, - "source": { - "type": "string", - "description": "Data source: vendor_reported, incident_derived, or poll_derived", - "nullable": true, - "example": "vendor_reported" - } - }, - "description": "Uptime response with per-bucket breakdown and overall percentage for the period" - }, - "SingleValueResponseServiceUptimeResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ServiceUptimeResponse" - } - } - }, - "UptimeBucketDto": { - "type": "object", - "properties": { - "timestamp": { - "type": "string", - "description": "Start of the bucket interval (ISO 8601)", - "format": "date-time", - "example": "2024-01-01T00:00:00Z" - }, - "uptimePct": { - "type": "number", - "description": "Uptime percentage for this bucket; null when no polls occurred", - "format": "double", - "nullable": true, - "example": 100.0 - }, - "totalPolls": { - "type": "integer", - "description": "Total number of polls recorded in this bucket", - "format": "int64", - "example": 12 - } - }, - "description": "Uptime statistics for a single time bucket" - }, - "TableValueResultScheduledMaintenanceDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ScheduledMaintenanceDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultServiceIncidentDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceIncidentDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "ServiceIncidentDetailDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "title": { - "type": "string" - }, - "status": { - "type": "string" - }, - "impact": { - "type": "string", - "nullable": true - }, - "startedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "resolvedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "detectedAt": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "shortlink": { - "type": "string", - "nullable": true - }, - "affectedComponents": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - }, - "updates": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceIncidentUpdateDto" - } - } - } - }, - "ServiceIncidentUpdateDto": { - "type": "object", - "properties": { - "status": { - "type": "string" - }, - "body": { - "type": "string", - "nullable": true - }, - "displayAt": { - "type": "string", - "format": "date-time", - "nullable": true - } - } - }, - "SingleValueResponseServiceIncidentDetailDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ServiceIncidentDetailDto" - } - } - }, - "TableValueResultServiceComponentDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceComponentDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "ComponentUptimeDayDto": { - "type": "object", - "properties": { - "date": { - "type": "string", - "description": "Date of the daily bucket (ISO 8601)", - "format": "date-time" - }, - "partialOutageSeconds": { - "type": "integer", - "description": "Seconds of partial outage observed on this day", - "format": "int32" - }, - "majorOutageSeconds": { - "type": "integer", - "description": "Seconds of major outage observed on this day", - "format": "int32" - }, - "uptimePercentage": { - "type": "number", - "description": "Computed uptime percentage for the day", - "format": "double" - }, - "eventsJson": { - "type": "string", - "description": "Incident event references for this day as raw JSON", - "nullable": true - }, - "source": { - "type": "string", - "description": "Data source: vendor_reported or incident_derived" - } - }, - "description": "Daily uptime data for a component" - }, - "TableValueResultComponentUptimeDayDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ComponentUptimeDayDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "GlobalStatusSummaryDto": { - "type": "object", - "properties": { - "totalServices": { - "type": "integer", - "description": "Total number of services in the catalog", - "format": "int32" - }, - "operationalCount": { - "type": "integer", - "description": "Number of services currently fully operational", - "format": "int32" - }, - "degradedCount": { - "type": "integer", - "description": "Number of services with degraded status", - "format": "int32" - }, - "partialOutageCount": { - "type": "integer", - "description": "Number of services with partial outage", - "format": "int32" - }, - "majorOutageCount": { - "type": "integer", - "description": "Number of services with major outage", - "format": "int32" - }, - "maintenanceCount": { - "type": "integer", - "description": "Number of services currently under maintenance", - "format": "int32" - }, - "activeIncidentCount": { - "type": "integer", - "description": "Total number of active incidents across all services", - "format": "int64" - }, - "servicesWithIssues": { - "type": "array", - "description": "Services that are not fully operational", - "items": { - "$ref": "#/components/schemas/ServiceCatalogDto" - } - } - }, - "description": "Global status summary across all subscribed vendor services" - }, - "SingleValueResponseGlobalStatusSummaryDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/GlobalStatusSummaryDto" - } - } - }, - "TableValueResultServiceSubscriptionDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ServiceSubscriptionDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultSecretDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SecretDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultResourceGroupDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ResourceGroupDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseResourceGroupHealthDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ResourceGroupHealthDto" - } - } - }, - "NotificationDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique notification identifier", - "format": "int64" - }, - "type": { - "type": "string", - "description": "Notification category (e.g. incident, monitor, team)" - }, - "title": { - "type": "string", - "description": "Short notification title" - }, - "body": { - "type": "string", - "description": "Full notification body; null for title-only notifications", - "nullable": true - }, - "resourceType": { - "type": "string", - "description": "Type of the resource this notification is about", - "nullable": true - }, - "resourceId": { - "type": "string", - "description": "ID of the resource this notification is about", - "nullable": true - }, - "read": { - "type": "boolean", - "description": "Whether the notification has been read" - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the notification was created", - "format": "date-time" - } - }, - "description": "In-app notification for the current user" - }, - "TableValueResultNotificationDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NotificationDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseLong": { - "type": "object", - "properties": { - "data": { - "type": "integer", - "format": "int64", - "nullable": true - } - } - }, - "TableValueResultNotificationPolicyDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NotificationPolicyDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultNotificationDispatchDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NotificationDispatchDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultMonitorDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MonitorDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "MonitorVersionDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique version record identifier", - "format": "uuid" - }, - "monitorId": { - "type": "string", - "description": "Monitor this version belongs to", - "format": "uuid" - }, - "version": { - "type": "integer", - "description": "Monotonically increasing version number", - "format": "int32" - }, - "snapshot": { - "$ref": "#/components/schemas/MonitorDto" - }, - "changedById": { - "type": "integer", - "description": "User ID who made the change; null for automated changes", - "format": "int32", - "nullable": true - }, - "changedVia": { - "type": "string", - "description": "Change source (DASHBOARD, CLI, API)", - "enum": [ - "API", - "DASHBOARD", - "CLI", - "TERRAFORM" - ] - }, - "changeSummary": { - "type": "string", - "description": "Human-readable description of what changed", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when this version was recorded", - "format": "date-time" - } - }, - "description": "A point-in-time version snapshot of a monitor configuration" - }, - "TableValueResultMonitorVersionDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MonitorVersionDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "SingleValueResponseMonitorVersionDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/MonitorVersionDto" - } - } - }, - "UptimeDto": { - "type": "object", - "properties": { - "uptimePercentage": { - "type": "number", - "description": "Uptime percentage over the requested window; null when no data", - "format": "double", - "nullable": true, - "example": 99.95 - }, - "totalChecks": { - "type": "integer", - "description": "Total number of checks executed", - "format": "int64", - "example": 1440 - }, - "passedChecks": { - "type": "integer", - "description": "Number of checks that passed", - "format": "int64", - "example": 1439 - }, - "avgLatencyMs": { - "type": "number", - "description": "Weighted average latency in milliseconds; null when no data", - "format": "double", - "nullable": true, - "example": 142.5 - }, - "p95LatencyMs": { - "type": "number", - "description": "95th-percentile latency in milliseconds (upper bound across regions); null when no data", - "format": "double", - "nullable": true, - "example": 312.0 - } - }, - "description": "Uptime statistics aggregated from continuous aggregates" - }, - "SingleValueResponseUptimeDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/UptimeDto" - } - } - }, - "CursorPage": { - "type": "object", - "properties": { - "data": { - "type": "array", - "description": "Items on this page", - "items": { - "type": "object", - "description": "Items on this page" - } - }, - "nextCursor": { - "type": "string", - "description": "Opaque cursor for the next page; null when there are no more results", - "nullable": true - }, - "hasMore": { - "type": "boolean", - "description": "Whether more results exist beyond this page" - } - }, - "description": "Cursor-paginated response for time-series and append-only data" - }, - "AssertionResultDto": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Assertion type", - "example": "status_code" - }, - "passed": { - "type": "boolean", - "description": "Whether the assertion passed" - }, - "severity": { - "type": "string", - "description": "Assertion severity", - "enum": [ - "fail", - "warn" - ] - }, - "message": { - "type": "string", - "description": "Human-readable result message", - "nullable": true - }, - "expected": { - "type": "string", - "description": "Expected value", - "nullable": true, - "example": "200" - }, - "actual": { - "type": "string", - "description": "Actual value observed", - "nullable": true, - "example": "503" - } - }, - "description": "Result of evaluating a single assertion against a check result" - }, - "CheckResultDetailsDto": { - "type": "object", - "properties": { - "statusCode": { - "type": "integer", - "description": "HTTP status code of the response", - "format": "int32", - "nullable": true, - "example": 200 - }, - "responseHeaders": { - "type": "object", - "additionalProperties": { - "type": "array", - "description": "HTTP response headers", - "nullable": true, - "items": { - "type": "string", - "description": "HTTP response headers", - "nullable": true - } - }, - "description": "HTTP response headers", - "nullable": true - }, - "responseBodySnapshot": { - "type": "string", - "description": "Raw response body snapshot (may be HTML, XML, JSON, or plain text)", - "nullable": true - }, - "assertionResults": { - "type": "array", - "description": "Individual assertion evaluation results", - "nullable": true, - "items": { - "$ref": "#/components/schemas/AssertionResultDto" - } - }, - "tlsInfo": { - "$ref": "#/components/schemas/TlsInfoDto" - }, - "redirectCount": { - "type": "integer", - "description": "Number of HTTP redirects followed", - "format": "int32", - "nullable": true, - "example": 2 - }, - "redirectTarget": { - "type": "string", - "description": "Final URL after redirects", - "nullable": true - }, - "responseSizeBytes": { - "type": "integer", - "description": "Response body size in bytes", - "format": "int32", - "nullable": true, - "example": 4096 - }, - "checkDetails": { - "oneOf": [ - { - "$ref": "#/components/schemas/Dns" - }, - { - "$ref": "#/components/schemas/Http" - }, - { - "$ref": "#/components/schemas/Icmp" - }, - { - "$ref": "#/components/schemas/McpServer" - }, - { - "$ref": "#/components/schemas/Tcp" - } - ] - } - }, - "description": "Type-specific details captured during a check execution" - }, - "CheckResultDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique identifier of the check result", - "format": "uuid" - }, - "timestamp": { - "type": "string", - "description": "Timestamp when the check was executed (ISO 8601)", - "format": "date-time" - }, - "region": { - "type": "string", - "description": "Region where the check was executed", - "example": "us-east" - }, - "responseTimeMs": { - "type": "integer", - "description": "Response time in milliseconds", - "format": "int32", - "nullable": true, - "example": 123 - }, - "passed": { - "type": "boolean", - "description": "Whether the check passed", - "example": true - }, - "failureReason": { - "type": "string", - "description": "Reason for failure when passed=false", - "nullable": true - }, - "severityHint": { - "type": "string", - "description": "Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", - "nullable": true - }, - "details": { - "$ref": "#/components/schemas/CheckResultDetailsDto" - } - }, - "description": "A single check result from a monitor run" - }, - "CheckTypeDetailsDto": { - "required": [ - "check_type" - ], - "type": "object", - "properties": { - "check_type": { - "type": "string" - } - }, - "description": "Check-type-specific details \u2014 polymorphic by check_type discriminator", - "discriminator": { - "propertyName": "check_type" - } - }, - "CursorPageCheckResultDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "description": "Items on this page", - "items": { - "$ref": "#/components/schemas/CheckResultDto" - } - }, - "nextCursor": { - "type": "string", - "description": "Opaque cursor for the next page; null when there are no more results", - "nullable": true - }, - "hasMore": { - "type": "boolean", - "description": "Whether more results exist beyond this page" - } - }, - "description": "Cursor-paginated response for time-series and append-only data" - }, - "Dns": { - "type": "object", - "description": "DNS check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "hostname": { - "type": "string", - "description": "Target hostname", - "nullable": true - }, - "requestedTypes": { - "type": "array", - "description": "Requested DNS record types", - "nullable": true, - "items": { - "type": "string", - "description": "Requested DNS record types", - "nullable": true - } - }, - "usedResolver": { - "type": "string", - "description": "Resolver used for lookup", - "nullable": true - }, - "records": { - "type": "object", - "additionalProperties": { - "type": "array", - "description": "Resolved DNS records keyed by record type", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Resolved DNS records keyed by record type", - "nullable": true - }, - "description": "Resolved DNS records keyed by record type", - "nullable": true - } - }, - "description": "Resolved DNS records keyed by record type", - "nullable": true - }, - "attempts": { - "type": "array", - "description": "DNS resolution attempts", - "nullable": true, - "items": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "DNS resolution attempts", - "nullable": true - }, - "description": "DNS resolution attempts", - "nullable": true - } - }, - "failureKind": { - "type": "string", - "description": "Kind of DNS failure, if any", - "nullable": true - } - } - } - ] - }, - "Http": { - "type": "object", - "description": "HTTP check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "timing": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Request phase timing breakdown", - "nullable": true - }, - "description": "Request phase timing breakdown", - "nullable": true - }, - "bodyTruncated": { - "type": "boolean", - "description": "Whether the response body was truncated before storage", - "nullable": true - } - } - } - ] - }, - "Icmp": { - "type": "object", - "description": "ICMP (ping) check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "Target host", - "example": "1.1.1.1" - }, - "packetsSent": { - "type": "integer", - "description": "Number of ICMP packets sent", - "format": "int32", - "nullable": true - }, - "packetsReceived": { - "type": "integer", - "description": "Number of ICMP packets received", - "format": "int32", - "nullable": true - }, - "packetLoss": { - "type": "number", - "description": "Packet loss percentage", - "format": "double", - "nullable": true, - "example": 0.0 - }, - "avgRttMs": { - "type": "number", - "description": "Average round-trip time in ms", - "format": "double", - "nullable": true - }, - "minRttMs": { - "type": "number", - "description": "Minimum round-trip time in ms", - "format": "double", - "nullable": true - }, - "maxRttMs": { - "type": "number", - "description": "Maximum round-trip time in ms", - "format": "double", - "nullable": true - }, - "jitterMs": { - "type": "number", - "description": "Jitter in ms", - "format": "double", - "nullable": true - } - } - } - ] - }, - "McpServer": { - "type": "object", - "description": "MCP server check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "MCP server URL", - "nullable": true - }, - "protocolVersion": { - "type": "string", - "description": "MCP protocol version", - "nullable": true - }, - "serverInfo": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "MCP server info (name, version, etc.)", - "nullable": true - }, - "description": "MCP server info (name, version, etc.)", - "nullable": true - }, - "toolCount": { - "type": "integer", - "description": "Number of tools exposed", - "format": "int32", - "nullable": true - }, - "resourceCount": { - "type": "integer", - "description": "Number of resources exposed", - "format": "int32", - "nullable": true - }, - "promptCount": { - "type": "integer", - "description": "Number of prompts exposed", - "format": "int32", - "nullable": true - } - } - } - ] - }, - "Tcp": { - "type": "object", - "description": "TCP check-type-specific details", - "allOf": [ - { - "$ref": "#/components/schemas/CheckTypeDetailsDto" - }, - { - "type": "object", - "properties": { - "host": { - "type": "string", - "description": "Target host", - "example": "db.example.com" - }, - "port": { - "type": "integer", - "description": "Target port", - "format": "int32", - "example": 5432 - }, - "connected": { - "type": "boolean", - "description": "Whether a TCP connection was established" - } - } - } - ] - }, - "TlsInfoDto": { - "type": "object", - "properties": { - "subjectCn": { - "type": "string", - "description": "Certificate subject common name", - "nullable": true, - "example": "*.example.com" - }, - "subjectSan": { - "type": "array", - "description": "Subject Alternative Names", - "nullable": true, - "items": { - "type": "string", - "description": "Subject Alternative Names", - "nullable": true - } - }, - "issuerCn": { - "type": "string", - "description": "Issuer common name", - "nullable": true, - "example": "R3" - }, - "issuerOrg": { - "type": "string", - "description": "Issuer organisation", - "nullable": true, - "example": "Let's Encrypt" - }, - "notBefore": { - "type": "string", - "description": "Certificate validity start (ISO 8601 UTC)", - "nullable": true - }, - "notAfter": { - "type": "string", - "description": "Certificate validity end (ISO 8601 UTC)", - "nullable": true - }, - "serialNumber": { - "type": "string", - "description": "Certificate serial number", - "nullable": true - }, - "tlsVersion": { - "type": "string", - "description": "TLS protocol version", - "nullable": true, - "example": "TLSv1.3" - }, - "cipherSuite": { - "type": "string", - "description": "Negotiated cipher suite", - "nullable": true - }, - "chainValid": { - "type": "boolean", - "description": "Whether the chain validated against the OS trust store", - "nullable": true - } - }, - "description": "TLS/SSL certificate details for HTTPS targets" - }, - "ChartBucketDto": { - "type": "object", - "properties": { - "bucket": { - "type": "string", - "description": "Start of the time bucket (ISO 8601)", - "format": "date-time", - "example": "2026-03-12T10:00:00Z" - }, - "uptimePercent": { - "type": "number", - "description": "Uptime percentage for this bucket; null when no data", - "format": "double", - "nullable": true, - "example": 100.0 - }, - "avgLatencyMs": { - "type": "number", - "description": "Weighted average latency in milliseconds for this bucket", - "format": "double", - "nullable": true, - "example": 120.3 - }, - "p95LatencyMs": { - "type": "number", - "description": "95th percentile latency in milliseconds (max across regions)", - "format": "double", - "nullable": true, - "example": 250.0 - }, - "p99LatencyMs": { - "type": "number", - "description": "99th percentile latency in milliseconds (max across regions)", - "format": "double", - "nullable": true, - "example": 480.0 - } - }, - "description": "Aggregated metrics for a time bucket" - }, - "RegionStatusDto": { - "type": "object", - "properties": { - "region": { - "type": "string", - "description": "Region identifier", - "example": "us-east" - }, - "passed": { - "type": "boolean", - "description": "Whether the last check in this region passed", - "example": true - }, - "responseTimeMs": { - "type": "integer", - "description": "Response time in milliseconds for the last check", - "format": "int32", - "nullable": true, - "example": 95 - }, - "timestamp": { - "type": "string", - "description": "Timestamp of the last check in this region (ISO 8601)", - "format": "date-time" - }, - "severityHint": { - "type": "string", - "description": "Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing", - "nullable": true - } - }, - "description": "Latest check result for a single region" - }, - "ResultSummaryDto": { - "type": "object", - "properties": { - "currentStatus": { - "type": "string", - "description": "Derived current status across all regions", - "enum": [ - "up", - "degraded", - "down", - "unknown" - ] - }, - "latestPerRegion": { - "type": "array", - "description": "Latest check result per region", - "items": { - "$ref": "#/components/schemas/RegionStatusDto" - } - }, - "chartData": { - "type": "array", - "description": "Time-bucketed chart data for the requested window", - "items": { - "$ref": "#/components/schemas/ChartBucketDto" - } - }, - "uptime24h": { - "type": "number", - "description": "Uptime percentage over the last 24 hours; null when no data", - "format": "double", - "nullable": true, - "example": 99.95 - }, - "uptimeWindow": { - "type": "number", - "description": "Uptime percentage for the selected chart window; null when no data", - "format": "double", - "nullable": true, - "example": 99.8 - } - }, - "description": "Dashboard summary: current status, per-region latest results, and chart data" - }, - "SingleValueResponseResultSummaryDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/ResultSummaryDto" - } - } - }, - "TableValueResultMaintenanceWindowDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MaintenanceWindowDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultInviteDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/InviteDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "IntegrationCatalogResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationDto" - } - } - } - }, - "IntegrationConfigSchemaDto": { - "type": "object", - "properties": { - "connectionFields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationFieldDto" - } - }, - "channelFields": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IntegrationFieldDto" - } - } - } - }, - "IntegrationDto": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "logoUrl": { - "type": "string" - }, - "authType": { - "type": "string" - }, - "tierAvailability": { - "type": "string", - "enum": [ - "FREE", - "STARTER", - "PRO", - "TEAM", - "BUSINESS", - "ENTERPRISE" - ] - }, - "lifecycle": { - "type": "string" - }, - "setupGuideUrl": { - "type": "string" - }, - "configSchema": { - "$ref": "#/components/schemas/IntegrationConfigSchemaDto" - } - } - }, - "IntegrationFieldDto": { - "required": [ - "key", - "label", - "required", - "sensitive", - "type" - ], - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "label": { - "type": "string" - }, - "type": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "sensitive": { - "type": "boolean" - }, - "placeholder": { - "type": "string", - "nullable": true - }, - "helpText": { - "type": "string", - "nullable": true - }, - "options": { - "type": "array", - "nullable": true, - "items": { - "type": "string", - "nullable": true - } - }, - "default": { - "type": "string", - "nullable": true - } - } - }, - "IncidentFilterParams": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "WATCHING", - "TRIGGERED", - "CONFIRMED", - "RESOLVED" - ] - }, - "severity": { - "type": "string", - "enum": [ - "DOWN", - "DEGRADED", - "MAINTENANCE" - ] - }, - "source": { - "type": "string", - "enum": [ - "AUTOMATIC", - "MANUAL", - "MONITORS", - "STATUS_DATA", - "RESOURCE_GROUP" - ] - }, - "monitorId": { - "type": "string", - "format": "uuid" - }, - "serviceId": { - "type": "string", - "format": "uuid" - }, - "resourceGroupId": { - "type": "string", - "format": "uuid" - }, - "tagId": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "environmentId": { - "type": "string", - "format": "uuid", - "nullable": true - }, - "startedFrom": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "startedTo": { - "type": "string", - "format": "date-time", - "nullable": true - }, - "page": { - "minimum": 0, - "type": "integer", - "format": "int32" - }, - "size": { - "maximum": 200, - "minimum": 1, - "type": "integer", - "format": "int32" - } - } - }, - "TableValueResultEnvironmentDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/EnvironmentDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "DashboardOverviewDto": { - "type": "object", - "properties": { - "monitors": { - "$ref": "#/components/schemas/MonitorsSummaryDto" - }, - "incidents": { - "$ref": "#/components/schemas/IncidentsSummaryDto" - } - }, - "description": "Combined dashboard overview for monitors and incidents" - }, - "IncidentsSummaryDto": { - "type": "object", - "properties": { - "active": { - "type": "integer", - "format": "int64" - }, - "resolvedToday": { - "type": "integer", - "format": "int64" - }, - "mttr30d": { - "type": "number", - "format": "double", - "nullable": true - } - }, - "description": "Incident summary counters" - }, - "MonitorsSummaryDto": { - "type": "object", - "properties": { - "total": { - "type": "integer", - "description": "Total number of monitors in the organization", - "format": "int64" - }, - "up": { - "type": "integer", - "description": "Number of monitors currently passing", - "format": "int64" - }, - "down": { - "type": "integer", - "description": "Number of monitors currently failing (DOWN severity)", - "format": "int64" - }, - "degraded": { - "type": "integer", - "description": "Number of monitors with degraded status", - "format": "int64" - }, - "paused": { - "type": "integer", - "description": "Number of disabled monitors", - "format": "int64" - }, - "avgUptime24h": { - "type": "number", - "description": "Average uptime percentage across all monitors over last 24h", - "format": "double", - "nullable": true - }, - "avgUptime30d": { - "type": "number", - "description": "Average uptime percentage across all monitors over last 30 days", - "format": "double", - "nullable": true - } - }, - "description": "Dashboard summary counters for monitors" - }, - "SingleValueResponseDashboardOverviewDto": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/DashboardOverviewDto" - } - } - }, - "CategoryDto": { - "type": "object", - "properties": { - "category": { - "type": "string", - "description": "Category name (e.g. CI/CD, Cloud, Payments)" - }, - "serviceCount": { - "type": "integer", - "description": "Number of services in this category", - "format": "int64" - } - }, - "description": "Service category with its count of catalog entries" - }, - "TableValueResultCategoryDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CategoryDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "AuthMeResponse": { - "type": "object", - "properties": { - "key": { - "$ref": "#/components/schemas/KeyInfo" - }, - "organization": { - "$ref": "#/components/schemas/OrgInfo" - }, - "plan": { - "$ref": "#/components/schemas/PlanInfo" - }, - "rateLimits": { - "$ref": "#/components/schemas/RateLimitInfo" - } - }, - "description": "Identity, organization, plan, and rate-limit info for the authenticated API key" - }, - "KeyInfo": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Key ID", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Human-readable key name" - }, - "createdAt": { - "type": "string", - "description": "When the key was created", - "format": "date-time" - }, - "expiresAt": { - "type": "string", - "description": "When the key expires (null = never)", - "format": "date-time", - "nullable": true - }, - "lastUsedAt": { - "type": "string", - "description": "Last time the key was used", - "format": "date-time", - "nullable": true - } - }, - "description": "API key metadata" - }, - "OrgInfo": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Organization ID", - "format": "int32" - }, - "name": { - "type": "string", - "description": "Organization name" - } - }, - "description": "Organization the key belongs to" - }, - "PlanInfo": { - "type": "object", - "properties": { - "tier": { - "type": "string", - "description": "Resolved plan tier", - "enum": [ - "FREE", - "STARTER", - "PRO", - "TEAM", - "BUSINESS", - "ENTERPRISE" - ] - }, - "subscriptionStatus": { - "type": "string", - "description": "Subscription status (null if no subscription)", - "nullable": true - }, - "trialActive": { - "type": "boolean", - "description": "Whether the org is on a trial" - }, - "trialExpiresAt": { - "type": "string", - "description": "Trial expiry (null if not trialing)", - "format": "date-time", - "nullable": true - }, - "entitlements": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/EntitlementDto" - }, - "description": "Entitlement limits keyed by entitlement name" - }, - "usage": { - "type": "object", - "additionalProperties": { - "type": "integer", - "description": "Current usage counters keyed by entitlement name", - "format": "int64" - }, - "description": "Current usage counters keyed by entitlement name" - } - }, - "description": "Billing plan and entitlement state" - }, - "RateLimitInfo": { - "type": "object", - "properties": { - "requestsPerMinute": { - "type": "integer", - "description": "Maximum requests allowed per window", - "format": "int64" - }, - "remaining": { - "type": "integer", - "description": "Requests remaining in the current window", - "format": "int64" - }, - "windowMs": { - "type": "integer", - "description": "Sliding window size in milliseconds", - "format": "int64" - } - }, - "description": "Rate-limit quota for the current sliding window" - }, - "SingleValueResponseAuthMeResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AuthMeResponse" - } - } - }, - "AuditEventDto": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "Unique audit event identifier", - "format": "int64" - }, - "actorId": { - "type": "integer", - "description": "User ID who performed the action; null for system actions", - "format": "int32", - "nullable": true - }, - "actorEmail": { - "type": "string", - "description": "Email of the actor; null for system actions", - "nullable": true - }, - "action": { - "type": "string", - "description": "Audit action type (e.g. monitor.created, api_key.revoked)" - }, - "resourceType": { - "type": "string", - "description": "Type of resource affected (e.g. monitor, api_key)", - "nullable": true - }, - "resourceId": { - "type": "string", - "description": "ID of the affected resource", - "nullable": true - }, - "resourceName": { - "type": "string", - "description": "Human-readable name of the affected resource", - "nullable": true - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "object", - "description": "Additional context about the action", - "nullable": true - }, - "description": "Additional context about the action", - "nullable": true - }, - "createdAt": { - "type": "string", - "description": "Timestamp when the action was performed", - "format": "date-time" - } - } - }, - "PageResultAuditEventDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AuditEventDto" - } - }, - "page": { - "type": "integer", - "format": "int32" - }, - "size": { - "type": "integer", - "format": "int32" - }, - "totalElements": { - "type": "integer", - "format": "int64" - }, - "totalPages": { - "type": "integer", - "format": "int32" - }, - "hasNext": { - "type": "boolean" - } - } - }, - "TableValueResultApiKeyDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ApiKeyDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "DeliveryAttemptDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "deliveryId": { - "type": "string", - "format": "uuid" - }, - "attemptNumber": { - "type": "integer", - "description": "1-based attempt number", - "format": "int32" - }, - "status": { - "type": "string", - "description": "Outcome: SUCCESS, FAILED, TIMEOUT, ERROR" - }, - "responseStatusCode": { - "type": "integer", - "description": "HTTP response status code from the external service", - "format": "int32", - "nullable": true - }, - "requestPayload": { - "type": "string", - "description": "JSON payload sent to the external service", - "nullable": true - }, - "responseBody": { - "type": "string", - "description": "Response body from the external service (truncated)", - "nullable": true - }, - "errorMessage": { - "type": "string", - "description": "Error message if the attempt failed", - "nullable": true - }, - "responseTimeMs": { - "type": "integer", - "description": "Round-trip time in milliseconds", - "format": "int32", - "nullable": true - }, - "externalId": { - "type": "string", - "description": "External identifier (e.g. PagerDuty dedup_key, SES MessageId, webhook delivery UUID)", - "nullable": true - }, - "requestHeaders": { - "type": "object", - "additionalProperties": { - "type": "string", - "description": "HTTP request headers sent to the external service", - "nullable": true - }, - "description": "HTTP request headers sent to the external service", - "nullable": true - }, - "attemptedAt": { - "type": "string", - "format": "date-time" - } - }, - "description": "Single delivery attempt with request/response audit data" - }, - "TableValueResultDeliveryAttemptDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeliveryAttemptDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultAlertChannelDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AlertChannelDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "TableValueResultAlertDeliveryDto": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/AlertDeliveryDto" - } - }, - "hasNext": { - "type": "boolean" - }, - "hasPrev": { - "type": "boolean" - } - } - }, - "RemoveMonitorTagsRequest": { - "required": [ - "tagIds" - ], - "type": "object", - "properties": { - "tagIds": { - "minItems": 1, - "type": "array", - "description": "IDs of the tags to detach from the monitor", - "items": { - "type": "string", - "description": "IDs of the tags to detach from the monitor", - "format": "uuid" - } - } - }, - "description": "Request body for removing tags from a monitor" - }, - "DeleteChannelResult": { - "type": "object", - "properties": { - "affectedPolicies": { - "type": "integer", - "description": "Number of notification policies whose escalation steps were modified", - "format": "int32" - }, - "disabledPolicies": { - "type": "integer", - "description": "Number of notification policies disabled because they had no remaining channels", - "format": "int32" - } - }, - "description": "Summary of policies affected by channel deletion" - } - }, - "securitySchemes": { - "BearerAuth": { - "type": "http", - "description": "API key (dh_live_...) or Auth0 JWT token", - "scheme": "bearer", - "bearerFormat": "JWT" - } - } - } -} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"DevHelm API","description":"DevHelm platform and public API","version":"1.0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"Heartbeat","description":"Public ping endpoint for heartbeat monitors"},{"name":"Invites","description":"Organization invite management"},{"name":"Onboarding","description":"User onboarding flow"},{"name":"Members","description":"Organization member management"},{"name":"Me","description":"Current user profile and organizations"},{"name":"Incidents","description":"Incident management and lifecycle"},{"name":"Maintenance Windows","description":"Schedule alert-suppression windows for monitors"},{"name":"Organizations","description":"Organization management"},{"name":"Integrations","description":"Static catalog of supported alert channel integrations"},{"name":"Incident Policies","description":"Manage trigger, confirmation, and recovery rules for monitors"},{"name":"Entitlements","description":"Plan entitlements and usage limits"},{"name":"Vault","description":"Organization vault management (admin-only)"},{"name":"Secrets","description":"Organization environment secret management"},{"name":"Transactions","description":"Subscription transaction history"},{"name":"Monitors","description":"Monitor CRUD and lifecycle management"},{"name":"Webhooks","description":"Webhook endpoint management, event catalog, and delivery history"},{"name":"Events","description":"Real-time event stream"},{"name":"Workspaces","description":"Workspace management within an organization"},{"name":"Notifications","description":"In-app notification center"},{"name":"Alert Channels","description":"Alert channel CRUD and connectivity testing"},{"name":"Subscriptions","description":"Organization subscription management"},{"name":"Service Subscriptions","description":"Manage which services an organization tracks"},{"name":"Tags","description":"Org-scoped tag management for monitors"},{"name":"Status Data","description":"Public service status catalog, components, uptime, and incident history"},{"name":"Check Results","description":"Query raw check results, uptime statistics, and summary data"},{"name":"API Keys","description":"Organization API key management"},{"name":"Dashboard","description":"Overview dashboard aggregates"},{"name":"Auth","description":"User registration"},{"name":"Monitor Auth","description":"Manage authentication configuration for a monitor"},{"name":"Audit Log","description":"Organization audit trail"},{"name":"Monitor Alert Channels","description":"Manage alert channel mappings for a monitor"},{"name":"Alert Deliveries","description":"Delivery audit trail: inspect per-attempt details for alert deliveries"},{"name":"API Auth","description":"Identity and quota info for API key authentication"},{"name":"Resource Groups","description":"Resource group CRUD and member management"},{"name":"Notification Policies","description":"Org-level notification routing policies with JSONB match rules"},{"name":"Notification Dispatches","description":"Dispatch debugging API: inspect which policies matched an incident and track delivery status"},{"name":"Environments","description":"Variable namespace management for monitors"},{"name":"Monitor Assertions","description":"Manage assertions for a monitor"},{"name":"Deploy Lock","description":"Mutex for CLI deploy operations"},{"name":"Billing","description":"Billing plans and pricing"}],"paths":{"/platform/orgs/{orgId}/subscriptions/{subscriptionId}":{"put":{"tags":["Subscriptions"],"summary":"Update subscription","operationId":"updateSubscription","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSubscriptionDto"}}}}}}},"/platform/onboarding/orgs/{orgId}/details":{"put":{"tags":["Onboarding"],"summary":"Update organization details","operationId":"updateOrgDetails","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/onboarding/advance":{"put":{"tags":["Onboarding"],"summary":"Advance onboarding stage forward","operationId":"advanceStage","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOnboardingStageRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/me":{"get":{"tags":["Me"],"summary":"Get current user","operationId":"me","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}},"put":{"tags":["Me"],"summary":"Update current user profile","operationId":"updateProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/me/notification-preferences":{"get":{"tags":["Me"],"summary":"Get current user's notification preferences","operationId":"getNotificationPreferences","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPreferencesDto"}}}}}},"put":{"tags":["Me"],"summary":"Update current user's notification preferences","operationId":"updateNotificationPreferences","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPreferencesRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPreferencesDto"}}}}}}},"/platform/admin/workspaces/{workspaceId}":{"get":{"tags":["admin-workspace-controller"],"operationId":"getWorkspace","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"put":{"tags":["admin-workspace-controller"],"operationId":"updateWorkspace","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"delete":{"tags":["admin-workspace-controller"],"operationId":"deleteWorkspace","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/admin/users/{userId}":{"put":{"tags":["admin-controller"],"operationId":"updateUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/admin/orgs/{orgId}":{"put":{"tags":["admin-controller"],"operationId":"updateOrganization","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/admin/orgs/{orgId}/members/{userId}/role":{"put":{"tags":["admin-member-controller"],"operationId":"updateMemberRole","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/workspaces/{workspaceId}":{"get":{"tags":["Workspaces"],"summary":"Get workspace by ID","operationId":"get","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"put":{"tags":["Workspaces"],"summary":"Update workspace","operationId":"update","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}},"delete":{"tags":["Workspaces"],"summary":"Delete workspace","operationId":"delete","parameters":[{"name":"workspaceId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Get a single webhook endpoint","operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}},"put":{"tags":["Webhooks"],"summary":"Update a webhook endpoint","operationId":"update_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","operationId":"delete_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/tags/{id}":{"get":{"tags":["Tags"],"summary":"Get a tag by ID","operationId":"getById","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTagDto"}}}}}},"put":{"tags":["Tags"],"summary":"Update a tag's name and/or color","operationId":"update_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTagRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTagDto"}}}}}},"delete":{"tags":["Tags"],"summary":"Delete a tag (cascades to all monitor associations)","operationId":"delete_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/secrets/{key}":{"put":{"tags":["Secrets"],"summary":"Update secret","operationId":"update_3","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSecretRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSecretDto"}}}}}},"delete":{"tags":["Secrets"],"summary":"Delete secret","operationId":"delete_3","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}":{"get":{"tags":["Resource Groups"],"summary":"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.","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"includeMetrics","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}},"put":{"tags":["Resource Groups"],"summary":"Update a resource group's name, description, alert policy, inherited settings, and health threshold","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateResourceGroupRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}},"delete":{"tags":["Resource Groups"],"summary":"Delete a resource group (cascades to member rows)","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/org":{"get":{"tags":["Organizations"],"summary":"Get the current organization","operationId":"get_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}},"put":{"tags":["Organizations"],"summary":"Update the current organization","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgDetailsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/api/v1/notifications/{id}/read":{"put":{"tags":["Notifications"],"summary":"Mark a notification as read","operationId":"markRead","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/notifications/read-all":{"put":{"tags":["Notifications"],"summary":"Mark all notifications as read","operationId":"markAllRead","responses":{"204":{"description":"No Content"}}}},"/api/v1/notification-policies/{id}":{"get":{"tags":["Notification Policies"],"summary":"Get a notification policy by ID","operationId":"getById_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}},"put":{"tags":["Notification Policies"],"summary":"Update a notification policy","operationId":"update_6","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateNotificationPolicyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}},"delete":{"tags":["Notification Policies"],"summary":"Delete a notification policy","operationId":"delete_5","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/policy":{"get":{"tags":["Incident Policies"],"summary":"Get incident policy for a monitor","description":"Returns the trigger rules, confirmation settings, and recovery settings for the given monitor.","operationId":"get_4","parameters":[{"name":"monitorId","in":"path","description":"Monitor UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Policy found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IncidentPolicyDto"}}}},"404":{"description":"Monitor or policy not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}},"put":{"tags":["Incident Policies"],"summary":"Update incident policy for a monitor","description":"Replaces the trigger rules, confirmation settings, and recovery settings. All fields are validated before saving.","operationId":"update_7","parameters":[{"name":"monitorId","in":"path","description":"Monitor UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"}}},"required":true},"responses":{"200":{"description":"Policy updated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IncidentPolicyDto"}}}},"400":{"description":"Validation error in JSONB shape","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}},"404":{"description":"Monitor or policy not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}}},"/api/v1/monitors/{monitorId}/auth":{"put":{"tags":["Monitor Auth"],"summary":"Update authentication config for a monitor","operationId":"update_8","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMonitorAuthRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}},"post":{"tags":["Monitor Auth"],"summary":"Set authentication config for a monitor","operationId":"set","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMonitorAuthRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}},"delete":{"tags":["Monitor Auth"],"summary":"Remove authentication config from a monitor","operationId":"remove","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/assertions/{assertionId}":{"put":{"tags":["Monitor Assertions"],"summary":"Update an assertion on a monitor","operationId":"update_9","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"assertionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssertionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAssertionDto"}}}}}},"delete":{"tags":["Monitor Assertions"],"summary":"Remove an assertion from a monitor","operationId":"remove_1","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"assertionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{monitorId}/alert-channels":{"put":{"tags":["Monitor Alert Channels"],"summary":"Replace the linked alert channel set for a monitor","operationId":"setChannels","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAlertChannelsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListUUID"}}}}}}},"/api/v1/monitors/{id}":{"get":{"tags":["Monitors"],"summary":"Get a single monitor by id","operationId":"get_5","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}},"put":{"tags":["Monitors"],"summary":"Update a monitor","operationId":"update_10","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMonitorRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}},"delete":{"tags":["Monitors"],"summary":"Soft-delete a monitor","operationId":"delete_6","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}/status":{"put":{"tags":["Members"],"summary":"Change member status","operationId":"changeStatus","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeStatusRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}/role":{"put":{"tags":["Members"],"summary":"Change member role","operationId":"changeRole","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeRoleRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/maintenance-windows/{id}":{"get":{"tags":["Maintenance Windows"],"summary":"Get a single maintenance window by ID","operationId":"getById_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}},"put":{"tags":["Maintenance Windows"],"summary":"Update a maintenance window","operationId":"update_11","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaintenanceWindowRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}},"delete":{"tags":["Maintenance Windows"],"summary":"Delete a maintenance window","operationId":"delete_7","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/environments/{slug}":{"get":{"tags":["Environments"],"summary":"Get environment by slug","operationId":"get_6","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}},"put":{"tags":["Environments"],"summary":"Update environment","operationId":"update_12","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}},"delete":{"tags":["Environments"],"summary":"Delete environment","operationId":"delete_8","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/alert-channels/{id}":{"put":{"tags":["Alert Channels"],"summary":"Update an alert channel's name and re-encrypt config","operationId":"update_13","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlertChannelRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertChannelDto"}}}}}},"delete":{"tags":["Alert Channels"],"summary":"Soft-delete an alert channel and return affected policy summary","operationId":"delete_9","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DeleteChannelResult"}}}}}}},"/v1/webhooks/paddle":{"post":{"tags":["paddle-webhook-controller"],"operationId":"handleWebhook","parameters":[{"name":"paddle-signature","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/v1/internal/workspaces":{"post":{"tags":["workspaces-controller"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceCreateParams"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/v1/internal/service-incidents":{"post":{"tags":["service-incident-internal-controller"],"operationId":"createOrResolve","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceIncidentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}}},"/v1/internal/resource-groups/services/{serviceId}/re-evaluate-health":{"post":{"tags":["resource-groups-internal-controller"],"operationId":"reEvaluateGroupHealthForService","parameters":[{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/resource-groups/monitors/{monitorId}/re-evaluate-health":{"post":{"tags":["resource-groups-internal-controller"],"operationId":"reEvaluateGroupHealthForMonitor","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/incidents":{"post":{"tags":["incidents-internal-controller"],"operationId":"createAutoIncident","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAutoIncidentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/incidents/{id}/resolve":{"post":{"tags":["incidents-internal-controller"],"operationId":"resolveAutoIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/incidents/{id}/reopen":{"post":{"tags":["incidents-internal-controller"],"operationId":"reopenAutoIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReopenAutoIncidentRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/escalation-tick":{"post":{"tags":["escalation-internal-controller"],"operationId":"runEscalationTick","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/billing/sync":{"post":{"tags":["admin-billing-controller"],"operationId":"syncFromPaddle","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInteger"}}}}}}},"/v1/internal/adapters/health":{"get":{"tags":["adapter-health-internal-controller"],"operationId":"getAllHealth","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAdapterHealthDto"}}}}}},"post":{"tags":["adapter-health-internal-controller"],"operationId":"reportOutcome","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdapterHealthReportRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdapterHealthDto"}}}}}}},"/platform/orgs":{"post":{"tags":["Organizations"],"summary":"Create organization","operationId":"create_1","parameters":[{"name":"ifNotExists","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrgRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseOrganizationDto"}}}}}}},"/platform/orgs/{orgId}/transactions":{"get":{"tags":["Transactions"],"summary":"List transactions","operationId":"list","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"limit","in":"query","required":false,"schema":{"maximum":100,"minimum":1,"type":"integer","format":"int32","default":10}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTransactionDto"}}}}}},"post":{"tags":["Transactions"],"summary":"Create subscription transaction","operationId":"createSubscriptionTransaction","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTransactionDto"}}}}}}},"/platform/onboarding/quick-monitor":{"post":{"tags":["Onboarding"],"summary":"Create a monitor with smart defaults from URL analysis","operationId":"quickMonitor","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuickMonitorRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/platform/onboarding/complete-setup":{"post":{"tags":["Onboarding"],"summary":"Complete onboarding setup (creates org + workspace, advances to FIRST_MONITOR)","operationId":"completeSetup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardingSetupRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/onboarding/analyze-url":{"post":{"tags":["Onboarding"],"summary":"Analyze a URL and return suggested monitor configuration","operationId":"analyzeUrl","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyzeUrlRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAnalyzeUrlResponse"}}}}}}},"/platform/invites/accept":{"post":{"tags":["Invites"],"summary":"Accept invite","operationId":"accept","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInviteRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAcceptInviteDto"}}}}}}},"/platform/auth/register":{"post":{"tags":["Auth"],"summary":"Register user","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterUserRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUserDto"}}}}}}},"/platform/admin/orgs/{orgId}/workspaces":{"get":{"tags":["admin-workspace-controller"],"operationId":"listWorkspaces","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}},"post":{"tags":["admin-workspace-controller"],"operationId":"createWorkspace","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/platform/admin/orgs/{orgId}/members":{"get":{"tags":["admin-member-controller"],"operationId":"listMembers","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMemberDto"}}}}}},"post":{"tags":["admin-member-controller"],"operationId":"addMember","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMemberRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMemberDto"}}}}}}},"/platform/admin/adapters/{serviceId}/enable":{"post":{"tags":["admin-adapter-health-controller"],"operationId":"reEnableAdapter","parameters":[{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdapterHealthDto"}}}}}}},"/api/v1/workspaces":{"get":{"tags":["Workspaces"],"summary":"List workspaces","operationId":"list_1","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}},"post":{"tags":["Workspaces"],"summary":"Create workspace","operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWorkspaceRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhook endpoints for the authenticated org","operationId":"list_2","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWebhookEndpointDto"}}}}}},"post":{"tags":["Webhooks"],"summary":"Register a new webhook endpoint","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookEndpointRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookEndpointDto"}}}}}}},"/api/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test delivery to a webhook endpoint","operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWebhookEndpointRequest"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookTestResult"}}}}}}},"/api/v1/webhooks/signing-secret/rotate":{"post":{"tags":["Webhooks"],"summary":"Generate or rotate the organization webhook signing secret","operationId":"rotateSigningSecret","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseString"}}}}}}},"/api/v1/vaults/rotate":{"post":{"tags":["Vault"],"summary":"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.","operationId":"rotateDek","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDekRotationResultDto"}}}}}}},"/api/v1/tags":{"get":{"tags":["Tags"],"summary":"List tags for the authenticated organization","operationId":"list_3","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"post":{"tags":["Tags"],"summary":"Create a new tag","operationId":"create_4","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTagRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTagDto"}}}}}}},"/api/v1/service-subscriptions/{slug}":{"post":{"tags":["Service Subscriptions"],"summary":"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.","operationId":"subscribe","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceSubscribeRequest"}}}},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}}},"/api/v1/secrets":{"get":{"tags":["Secrets"],"summary":"List secrets","operationId":"list_4","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultSecretDto"}}}}}},"post":{"tags":["Secrets"],"summary":"Create secret","operationId":"create_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSecretRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseSecretDto"}}}}}}},"/api/v1/resource-groups":{"get":{"tags":["Resource Groups"],"summary":"List all resource groups for the authenticated org with health summaries","operationId":"list_5","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultResourceGroupDto"}}}}}},"post":{"tags":["Resource Groups"],"summary":"Create a new resource group","operationId":"create_6","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResourceGroupRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupDto"}}}}}}},"/api/v1/resource-groups/{id}/members":{"post":{"tags":["Resource Groups"],"summary":"Add a monitor or service member to a resource group","operationId":"addMember_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddResourceGroupMemberRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupMemberDto"}}}}}}},"/api/v1/notification-policies":{"get":{"tags":["Notification Policies"],"summary":"List all notification policies for the authenticated org","operationId":"list_6","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationPolicyDto"}}}}}},"post":{"tags":["Notification Policies"],"summary":"Create a notification policy with match rules and escalation chain","operationId":"create_7","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotificationPolicyRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationPolicyDto"}}}}}}},"/api/v1/notification-policies/{id}/test":{"post":{"tags":["Notification Policies"],"summary":"Dry-run: evaluate a policy's match rules against a supplied incident context","operationId":"test_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestNotificationPolicyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestMatchResult"}}}}}}},"/api/v1/notification-dispatches/{id}/acknowledge":{"post":{"tags":["Notification Dispatches"],"summary":"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).","operationId":"acknowledge","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationDispatchDto"}}}}}}},"/api/v1/monitors":{"get":{"tags":["Monitors"],"summary":"List monitors for the authenticated org","operationId":"list_7","parameters":[{"name":"enabled","in":"query","description":"Filter by enabled state","required":false,"schema":{"type":"boolean"}},{"name":"type","in":"query","description":"Filter by monitor type","required":false,"schema":{"type":"string","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]}},{"name":"managedBy","in":"query","description":"Filter by managed-by source","required":false,"schema":{"type":"string","enum":["DASHBOARD","CLI"]}},{"name":"tags","in":"query","description":"Filter by tag names, comma-separated (e.g. prod,critical)","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Case-insensitive name search","required":false,"schema":{"type":"string"}},{"name":"environmentId","in":"query","description":"Filter by environment ID","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMonitorDto"}}}}}},"post":{"tags":["Monitors"],"summary":"Create a new monitor","operationId":"create_8","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMonitorRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{monitorId}/assertions":{"post":{"tags":["Monitor Assertions"],"summary":"Add an assertion to a monitor","operationId":"add","parameters":[{"name":"monitorId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAssertionRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAssertionDto"}}}}}}},"/api/v1/monitors/{id}/test":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"testExisting","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorTestResultDto"}}}}}}},"/api/v1/monitors/{id}/tags":{"get":{"tags":["Monitors"],"summary":"Get all tags applied to a monitor","operationId":"getMonitorTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"post":{"tags":["Monitors"],"summary":"Add tags to a monitor; supports existing tag IDs and inline creation of new tags","operationId":"addMonitorTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultTagDto"}}}}}},"delete":{"tags":["Monitors"],"summary":"Remove tags from a monitor by their IDs","operationId":"removeMonitorTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveMonitorTagsRequest"}}},"required":true},"responses":{"204":{"description":"No Content"}}}},"/api/v1/monitors/{id}/rotate-token":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"rotateToken","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{id}/resume":{"post":{"tags":["Monitors"],"summary":"Resume a monitor (set enabled=true)","operationId":"resume","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/{id}/pause":{"post":{"tags":["Monitors"],"summary":"Pause a monitor (set enabled=false)","operationId":"pause","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorDto"}}}}}}},"/api/v1/monitors/test":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"testAdHoc","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitorTestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorTestResultDto"}}}}}}},"/api/v1/monitors/bulk":{"post":{"tags":["Monitors"],"summary":"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.","operationId":"bulkAction","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkMonitorActionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseBulkMonitorActionResult"}}}}}}},"/api/v1/maintenance-windows":{"get":{"tags":["Maintenance Windows"],"summary":"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).","operationId":"list_8","parameters":[{"name":"monitorId","in":"query","description":"Filter by monitor UUID","required":false,"schema":{"type":"string","format":"uuid"}},{"name":"filter","in":"query","description":"Filter by status: 'active' or 'upcoming'","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMaintenanceWindowDto"}}}}}},"post":{"tags":["Maintenance Windows"],"summary":"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.","operationId":"create_9","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaintenanceWindowRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMaintenanceWindowDto"}}}}}}},"/api/v1/invites":{"get":{"tags":["Invites"],"summary":"List invites","operationId":"list_9","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultInviteDto"}}}}}},"post":{"tags":["Invites"],"summary":"Create invite","operationId":"create_10","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInviteRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInviteDto"}}}}}}},"/api/v1/invites/{inviteId}/revoke":{"post":{"tags":["Invites"],"summary":"Revoke invite","operationId":"revoke","parameters":[{"name":"inviteId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/invites/{inviteId}/resend":{"post":{"tags":["Invites"],"summary":"Resend invite","operationId":"resend","parameters":[{"name":"inviteId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseInviteDto"}}}}}}},"/api/v1/incidents":{"get":{"tags":["Incidents"],"summary":"List incidents for the authenticated org","operationId":"list_10","parameters":[{"name":"params","in":"query","required":true,"schema":{"$ref":"#/components/schemas/IncidentFilterParams"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}},"post":{"tags":["Incidents"],"summary":"Create a manual incident","operationId":"create_11","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateManualIncidentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/incidents/{id}/updates":{"post":{"tags":["Incidents"],"summary":"Add an update to an incident (optionally change status)","operationId":"addUpdate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddIncidentUpdateRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/incidents/{id}/resolve":{"post":{"tags":["Incidents"],"summary":"Resolve an incident","operationId":"resolve","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveIncidentRequest"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/heartbeat/{token}":{"get":{"tags":["Heartbeat"],"summary":"Record a heartbeat ping (GET)","description":"Called by external systems (cron jobs, scheduled tasks) to signal liveness. Always returns 200 OK.","operationId":"pingGet","parameters":[{"name":"token","in":"path","description":"Ping endpoint token for the heartbeat monitor","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}},"post":{"tags":["Heartbeat"],"summary":"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.","operationId":"pingPost","parameters":[{"name":"token","in":"path","description":"Ping endpoint token for the heartbeat monitor","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"string"}},"text/plain":{"schema":{"type":"string"}},"*/*":{"schema":{"type":"string"}}}},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"boolean"}}}}}}}},"/api/v1/environments":{"get":{"tags":["Environments"],"summary":"List environments","operationId":"list_11","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultEnvironmentDto"}}}}}},"post":{"tags":["Environments"],"summary":"Create environment","operationId":"create_12","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEnvironmentDto"}}}}}}},"/api/v1/deploy/lock":{"get":{"tags":["Deploy Lock"],"summary":"Get current deploy lock","description":"Returns the active deploy lock for the current workspace, if any.","operationId":"current","parameters":[{"name":"x-phelm-workspace-id","in":"header","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDeployLockDto"}}}}}},"post":{"tags":["Deploy Lock"],"summary":"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.","operationId":"acquire","parameters":[{"name":"x-phelm-workspace-id","in":"header","description":"Target workspace ID (defaults to 1)","required":false,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcquireDeployLockRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDeployLockDto"}}}}}}},"/api/v1/api-keys":{"get":{"tags":["API Keys"],"summary":"List API keys","operationId":"list_12","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultApiKeyDto"}}}}}},"post":{"tags":["API Keys"],"summary":"Create API key","operationId":"create_13","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyCreateResponse"}}}}}}},"/api/v1/api-keys/{id}/revoke":{"post":{"tags":["API Keys"],"summary":"Revoke API key","operationId":"revoke_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyDto"}}}}}}},"/api/v1/api-keys/{id}/regenerate":{"post":{"tags":["API Keys"],"summary":"Regenerate API key","operationId":"regenerate","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyCreateResponse"}}}}}}},"/api/v1/alert-deliveries/{id}/retry":{"post":{"tags":["Alert Deliveries"],"summary":"Retry a failed delivery","description":"Resets a FAILED delivery to RETRY_PENDING so the delivery worker re-attempts it.","operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertDeliveryDto"}}}}}}},"/api/v1/alert-channels":{"get":{"tags":["Alert Channels"],"summary":"List active alert channels for the authenticated org","operationId":"list_13","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAlertChannelDto"}}}}}},"post":{"tags":["Alert Channels"],"summary":"Create a new alert channel with encrypted config","operationId":"create_14","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlertChannelRequest"}}},"required":true},"responses":{"201":{"description":"Created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAlertChannelDto"}}}}}}},"/api/v1/alert-channels/{id}/test":{"post":{"tags":["Alert Channels"],"summary":"Test a saved alert channel's connectivity","operationId":"test_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestChannelResult"}}}}}}},"/api/v1/alert-channels/test":{"post":{"tags":["Alert Channels"],"summary":"Test alert channel connectivity using raw config (no saved channel required)","operationId":"testConfig","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestAlertChannelRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseTestChannelResult"}}}}}}},"/v1/internal/service-incidents/by-ref/{serviceId}/{externalRef}/components":{"patch":{"tags":["service-incident-internal-controller"],"operationId":"addComponents","parameters":[{"name":"serviceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"externalRef","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComponentUpdateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIncidentDto"}}}}}}},"/api/v1/service-subscriptions/{id}/alert-sensitivity":{"patch":{"tags":["Service Subscriptions"],"summary":"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).","operationId":"updateAlertSensitivity","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlertSensitivityRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}}},"/api/v1/api-keys/{id}":{"delete":{"tags":["API Keys"],"summary":"Delete API key","operationId":"delete_10","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}},"patch":{"tags":["API Keys"],"summary":"Update API key","operationId":"update_14","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApiKeyRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseApiKeyDto"}}}}}}},"/v1/internal/workspaces/{id}":{"get":{"tags":["workspaces-controller"],"operationId":"get_7","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWorkspaceDto"}}}}}}},"/v1/internal/orgs/{id}/workspaces":{"get":{"tags":["orgs-controller"],"operationId":"listWorkspaces_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWorkspaceDto"}}}}}}},"/v1/internal/monitors/{id}/policy":{"get":{"tags":["monitors-internal-controller"],"operationId":"policy","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentPolicyDto"}}}}}}},"/v1/internal/monitors/{id}/env-variables":{"get":{"tags":["monitors-internal-controller"],"operationId":"getEnvVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMapStringString"}}}}}}},"/v1/internal/monitors/{id}/auth":{"get":{"tags":["monitors-internal-controller"],"operationId":"auth","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorAuthDto"}}}}}}},"/v1/internal/monitors/{id}/assertions":{"get":{"tags":["monitors-internal-controller"],"operationId":"getAssertions","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListMonitorAssertionDto"}}}}}}},"/v1/internal/monitors/{id}/active-incident":{"get":{"tags":["monitors-internal-controller"],"operationId":"activeIncident","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDto"}}}}}}},"/v1/internal/monitors/schedulable":{"get":{"tags":["monitors-internal-controller"],"operationId":"schedulable","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SchedulableMonitorDto"}}}}}}}},"/platform/plans":{"get":{"tags":["Billing"],"summary":"List public billing plans","operationId":"getPublicPlans","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseListBillingPlanDto"}}}}}}},"/platform/orgs/{orgId}/subscriptions":{"get":{"tags":["Subscriptions"],"summary":"List active subscriptions","operationId":"listActive","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultSubscriptionDto"}}}}}},"delete":{"tags":["Subscriptions"],"operationId":"cancel","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/orgs/{orgId}/subscriptions/upcoming-charge":{"get":{"tags":["Subscriptions"],"summary":"Get upcoming charge","operationId":"getUpcomingCharge","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"priceId","in":"query","required":true,"schema":{"minimum":1,"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUpcomingChargeResponse"}}}}}}},"/platform/orgs/{orgId}/subscriptions/management-urls":{"get":{"tags":["Subscriptions"],"summary":"Get subscription management URLs","operationId":"getManagementUrls","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMapStringString"}}}}}}},"/platform/orgs/{orgId}/subscriptions/customer-auth-token":{"get":{"tags":["Subscriptions"],"summary":"Get customer auth token","operationId":"getCustomerAuthToken","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseString"}}}}}}},"/platform/orgs/{orgId}/entitlements":{"get":{"tags":["Entitlements"],"summary":"Get resolved entitlements and current usage for the organization","operationId":"getEntitlements","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseEntitlementResponse"}}}}}}},"/platform/orgs/search":{"get":{"tags":["Organizations"],"summary":"Search organizations","operationId":"searchOrganizations","parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string"}},{"name":"paginationParams","in":"query","required":true,"schema":{"$ref":"#/components/schemas/PaginationParams"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultIdValuePair"}}}}}}},"/platform/me/orgs":{"get":{"tags":["Me"],"summary":"Get current user's organizations","operationId":"myOrgs","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMyOrgItemDto"}}}}}}},"/platform/events/stream":{"get":{"tags":["Events"],"summary":"Subscribe to real-time platform events via SSE","operationId":"stream","responses":{"200":{"description":"OK","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/platform/admin/users":{"get":{"tags":["admin-controller"],"operationId":"listUsers","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultUserDto"}}}}}}},"/platform/admin/stats":{"get":{"tags":["admin-controller"],"operationId":"getStats","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAdminStatsDto"}}}}}}},"/platform/admin/orgs":{"get":{"tags":["admin-controller"],"operationId":"listOrgs","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultOrganizationDto"}}}}}}},"/platform/admin/adapters/health":{"get":{"tags":["admin-adapter-health-controller"],"operationId":"getAdapterHealth","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAdapterHealthDto"}}}}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"List recent deliveries for a webhook endpoint","operationId":"listDeliveries","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultWebhookDeliveryDto"}}}}}}},"/api/v1/webhooks/signing-secret":{"get":{"tags":["Webhooks"],"summary":"Get signing secret metadata for the authenticated org","operationId":"getSigningSecretInfo","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseWebhookSigningSecretDto"}}}}}}},"/api/v1/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"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.","operationId":"listEvents","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/WebhookEventCatalogResponse"}}}}}}},"/api/v1/services":{"get":{"tags":["Status Data"],"summary":"List all enabled services (cursor-paginated)","operationId":"listServices","parameters":[{"name":"category","in":"query","description":"Filter by category (exact match)","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by current overall_status (exact match)","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Page size (1–100, default 20)","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageServiceCatalogDto"}}}}}}},"/api/v1/services/{slugOrId}":{"get":{"tags":["Status Data"],"summary":"Get a single service by slug or UUID with current status, components, and recent incidents","operationId":"getService","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceDetailDto"}}}}}}},"/api/v1/services/{slugOrId}/uptime":{"get":{"tags":["Status Data"],"summary":"Get uptime statistics for a service","description":"Uptime data aggregated across active non-group components.","operationId":"getServiceUptime","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"period","in":"query","description":"Time window","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d","1y","2y","all"]}},{"name":"granularity","in":"query","description":"Bucket granularity","required":false,"schema":{"type":"string","enum":["hourly","daily","monthly"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceUptimeResponse"}}}}},"security":[{"BearerAuth":[]}]}},"/api/v1/services/{slugOrId}/maintenances":{"get":{"tags":["Status Data"],"summary":"List scheduled maintenances for a service","operationId":"getScheduledMaintenances","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by status (e.g. scheduled, in_progress, verifying, completed)","required":false,"schema":{"type":"array","items":{"type":"string"}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultScheduledMaintenanceDto"}}}}}}},"/api/v1/services/{slugOrId}/incidents":{"get":{"tags":["Status Data"],"summary":"List incident history for a service (paginated)","operationId":"listIncidents","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","description":"Earliest start date (ISO 8601 date)","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","description":"Filter: active (unresolved), resolved, or omit for all","required":false,"schema":{"type":"string","enum":["active","resolved"]}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceIncidentDto"}}}}}}},"/api/v1/services/{slugOrId}/incidents/{incidentId}":{"get":{"tags":["Status Data"],"summary":"Get incident detail with full update timeline","operationId":"getIncident","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"incidentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceIncidentDetailDto"}}}}}}},"/api/v1/services/{slugOrId}/components":{"get":{"tags":["Status Data"],"summary":"List active components for a service with current status and inline uptime","operationId":"getComponents","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceComponentDto"}}}}}}},"/api/v1/services/{slugOrId}/components/{componentId}/uptime":{"get":{"tags":["Status Data"],"summary":"Get daily uptime data for a component","operationId":"getComponentUptime","parameters":[{"name":"slugOrId","in":"path","required":true,"schema":{"type":"string"}},{"name":"componentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"period","in":"query","description":"Time window","required":false,"schema":{"type":"string","enum":["7d","30d","90d","1y"]}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultComponentUptimeDayDto"}}}}}}},"/api/v1/services/summary":{"get":{"tags":["Status Data"],"summary":"Global status summary across all services","description":"Returns aggregate counts of services by status and a list of services currently experiencing issues.","operationId":"getGlobalStatusSummary","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseGlobalStatusSummaryDto"}}}}}}},"/api/v1/services/incidents":{"get":{"tags":["Status Data"],"summary":"List vendor incidents across all services (paginated)","description":"Cross-service vendor incident feed ordered by start date descending.","operationId":"listCrossServiceIncidents","parameters":[{"name":"from","in":"query","description":"Earliest start date (ISO 8601 date)","required":false,"schema":{"type":"string","format":"date"}},{"name":"status","in":"query","description":"Filter: active (unresolved), resolved, or omit for all","required":false,"schema":{"type":"string","enum":["active","resolved"]}},{"name":"category","in":"query","description":"Filter by service category","required":false,"schema":{"type":"string"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceIncidentDto"}}}}}}},"/api/v1/service-subscriptions":{"get":{"tags":["Service Subscriptions"],"summary":"List all service subscriptions for the organization","operationId":"list_14","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultServiceSubscriptionDto"}}}}}}},"/api/v1/service-subscriptions/{id}":{"get":{"tags":["Service Subscriptions"],"summary":"Get a subscription by its ID","operationId":"get_8","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseServiceSubscriptionDto"}}}}}},"delete":{"tags":["Service Subscriptions"],"summary":"Remove a subscription by its ID","description":"Removes a specific subscription (whole-service or component-level). No-op if not found.","operationId":"unsubscribe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}/health":{"get":{"tags":["Resource Groups"],"summary":"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.","operationId":"getHealth","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResourceGroupHealthDto"}}}}}}},"/api/v1/notifications":{"get":{"tags":["Notifications"],"summary":"List notifications for the current user","operationId":"list_15","parameters":[{"name":"unreadOnly","in":"query","required":false,"schema":{"type":"boolean","default":false}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDto"}}}}}}},"/api/v1/notifications/unread-count":{"get":{"tags":["Notifications"],"summary":"Get unread notification count","operationId":"unreadCount","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseLong"}}}}}}},"/api/v1/notification-policies/{id}/dispatches":{"get":{"tags":["Notification Policies"],"summary":"List all dispatches (firing history) for a notification policy","operationId":"listDispatches","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDispatchDto"}}}}}}},"/api/v1/notification-dispatches":{"get":{"tags":["Notification Dispatches"],"summary":"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.","operationId":"listByIncident","parameters":[{"name":"incident_id","in":"query","description":"UUID of the incident to inspect","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultNotificationDispatchDto"}}}}}}},"/api/v1/notification-dispatches/{id}":{"get":{"tags":["Notification Dispatches"],"summary":"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.","operationId":"getById_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseNotificationDispatchDto"}}}}}}},"/api/v1/monitors/{id}/versions":{"get":{"tags":["Monitors"],"summary":"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.","operationId":"listVersions","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMonitorVersionDto"}}}}}}},"/api/v1/monitors/{id}/versions/{version}":{"get":{"tags":["Monitors"],"summary":"Get a specific version snapshot for a monitor","description":"Returns the full monitor config snapshot captured at the given version number.","operationId":"getVersion","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"version","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseMonitorVersionDto"}}}}}}},"/api/v1/monitors/{id}/uptime":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getUptime","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Time window for uptime calculation","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d"]}}],"responses":{"200":{"description":"Uptime statistics","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UptimeDto"}}}},"400":{"description":"Invalid window parameter","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseUptimeDto"}}}}}}},"/api/v1/monitors/{id}/results":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getResults","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"from","in":"query","description":"Start of time range (ISO 8601, inclusive); defaults to 24 hours ago","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"End of time range (ISO 8601, inclusive); defaults to now","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"cursor","in":"query","description":"Opaque cursor from a previous response for pagination","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Maximum results per page (1–200)","required":false,"schema":{"type":"integer","format":"int32","default":50},"example":50},{"name":"region","in":"query","description":"Filter by region (e.g. us-east)","required":false,"schema":{"type":"string"}},{"name":"passed","in":"query","description":"Filter by pass/fail status","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Paginated check results","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPage"}}}},"400":{"description":"Invalid query parameters","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CursorPageCheckResultDto"}}}}}}},"/api/v1/monitors/{id}/results/summary":{"get":{"tags":["Check Results"],"summary":"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.","operationId":"getSummary","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"chartWindow","in":"query","description":"Chart window: 24h returns hourly buckets, 7d/30d/90d return daily buckets","required":false,"schema":{"type":"string","enum":["24h","7d","30d","90d"]}}],"responses":{"200":{"description":"Results summary","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ResultSummaryDto"}}}},"400":{"description":"Invalid chartWindow parameter","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}},"403":{"description":"Monitor does not belong to the caller's org","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}},"404":{"description":"Monitor not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseResultSummaryDto"}}}}}}},"/api/v1/members":{"get":{"tags":["Members"],"summary":"List organization members","operationId":"list_16","parameters":[{"name":"pageable","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Pageable"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultMemberDto"}}}}}}},"/api/v1/integrations":{"get":{"tags":["Integrations"],"summary":"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.","operationId":"list_17","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IntegrationCatalogResponse"}}}}}}},"/api/v1/incidents/{id}":{"get":{"tags":["Incidents"],"summary":"Get incident details including update timeline","operationId":"get_9","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseIncidentDetailDto"}}}}}}},"/api/v1/dashboard/overview":{"get":{"tags":["Dashboard"],"summary":"Dashboard overview","description":"Returns monitor status counts, average uptime windows, and incident aggregates for the authenticated org. Results are cached for 1 minute.","operationId":"overview","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseDashboardOverviewDto"}}}}}}},"/api/v1/categories":{"get":{"tags":["Status Data"],"summary":"List categories with service counts","operationId":"listCategories","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultCategoryDto"}}}}}}},"/api/v1/auth/me":{"get":{"tags":["API Auth"],"summary":"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_...).","operationId":"me_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SingleValueResponseAuthMeResponse"}}}}}}},"/api/v1/audit-log":{"get":{"tags":["Audit Log"],"summary":"List audit events for the current organization","operationId":"list_18","parameters":[{"name":"action","in":"query","required":false,"schema":{"type":"string"}},{"name":"actorId","in":"query","required":false,"schema":{"type":"integer","format":"int32"}},{"name":"resourceType","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PageResultAuditEventDto"}}}}}}},"/api/v1/alert-deliveries/{id}/attempts":{"get":{"tags":["Alert Deliveries"],"summary":"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.","operationId":"listAttempts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultDeliveryAttemptDto"}}}}}}},"/api/v1/alert-channels/{id}/deliveries":{"get":{"tags":["Alert Channels"],"summary":"List delivery history for an alert channel","operationId":"listDeliveries_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TableValueResultAlertDeliveryDto"}}}}}}},"/platform/orgs/{orgId}":{"delete":{"tags":["Organizations"],"summary":"Delete organization","operationId":"delete_11","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/platform/admin/orgs/{orgId}/members/{userId}":{"delete":{"tags":["admin-member-controller"],"operationId":"removeMember","parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}},{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/resource-groups/{id}/members/{memberId}":{"delete":{"tags":["Resource Groups"],"summary":"Remove a member from a resource group","operationId":"removeMember_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"memberId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/members/{userId}":{"delete":{"tags":["Members"],"summary":"Remove member from organization","operationId":"remove_2","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/deploy/lock/{lockId}":{"delete":{"tags":["Deploy Lock"],"summary":"Release deploy lock","description":"Releases a deploy lock by ID. Only the lock holder should call this.","operationId":"release","parameters":[{"name":"lockId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"x-phelm-workspace-id","in":"header","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}},"/api/v1/deploy/lock/force":{"delete":{"tags":["Deploy Lock"],"summary":"Force-release deploy lock","description":"Forcibly removes any deploy lock on the current workspace. Use to break stale locks.","operationId":"forceRelease","parameters":[{"name":"x-phelm-workspace-id","in":"header","required":false,"schema":{"type":"integer","format":"int32"}}],"responses":{"204":{"description":"No Content"}}}}},"components":{"schemas":{"CreateSubscriptionRequest":{"type":"object","properties":{"priceId":{"minimum":1,"type":"integer","format":"int32"}}},"BillingPlanDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique billing plan identifier","format":"int32"},"paddleId":{"type":"string","description":"Paddle product identifier"},"name":{"type":"string","description":"Billing plan display name"},"description":{"type":"string","description":"Plan description","nullable":true},"prices":{"type":"array","description":"Available prices for this plan; null when not requested","nullable":true,"items":{"$ref":"#/components/schemas/BillingPriceDto"}}},"description":"Associated billing plan; null when not requested","nullable":true},"BillingPriceDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique billing price identifier","format":"int32"},"paddleId":{"type":"string","description":"Paddle price identifier"},"amount":{"type":"integer","description":"Price amount in smallest currency unit (e.g. cents)","format":"int32"},"interval":{"type":"string","description":"Billing interval (MONTH or YEAR)","enum":["DAY","WEEK","MONTH","YEAR"]},"intervalCount":{"type":"integer","description":"Number of intervals between billing cycles","format":"int32"},"description":{"type":"string","description":"Price description","nullable":true},"billingPlan":{"$ref":"#/components/schemas/BillingPlanDto"}},"description":"Price details for this line item"},"ItemDto":{"type":"object","properties":{"billingPrice":{"$ref":"#/components/schemas/BillingPriceDto"},"quantity":{"type":"integer","description":"Quantity of this price","format":"int32"},"amount":{"type":"integer","description":"Line item total in smallest currency unit","format":"int32"}},"description":"Line items included in this subscription"},"SingleValueResponseSubscriptionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SubscriptionDto"}}},"SubscriptionDto":{"type":"object","properties":{"id":{"type":"integer","description":"Internal subscription identifier","format":"int32"},"paddleId":{"type":"string","description":"Paddle subscription identifier"},"createdAt":{"type":"string","description":"Timestamp when the subscription was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the subscription was last updated","format":"date-time"},"organizationId":{"type":"integer","description":"Organization this subscription belongs to","format":"int32"},"status":{"type":"string","description":"Current subscription status","enum":["ACTIVE","CANCELED","PAST_DUE","PAUSED","TRIALING"]},"nextBilledAt":{"type":"string","description":"Next billing date; null when cancelled or expired","format":"date-time","nullable":true},"willCancelAt":{"type":"string","description":"Scheduled cancellation date; null if no cancellation pending","format":"date-time","nullable":true},"items":{"type":"array","description":"Line items included in this subscription","items":{"$ref":"#/components/schemas/ItemDto"}}},"description":"Current billing subscription details"},"UpdateOrgDetailsRequest":{"required":["email","name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New organization name (max 200 chars)"},"email":{"minLength":1,"type":"string","description":"New billing and contact email address","format":"email"},"size":{"maxLength":50,"minLength":0,"type":"string","description":"Team size range (e.g. 1-10, 11-50)"},"industry":{"maxLength":100,"minLength":0,"type":"string","description":"Industry vertical (e.g. SaaS, Fintech)"},"websiteUrl":{"maxLength":255,"minLength":0,"type":"string","description":"Organization website URL (max 255 chars)"}}},"OrganizationDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique organization identifier","format":"int32"},"name":{"type":"string","description":"Organization name"},"email":{"type":"string","description":"Billing and contact email","nullable":true},"size":{"type":"string","description":"Team size range (e.g. 1-10, 11-50)","nullable":true},"industry":{"type":"string","description":"Industry vertical (e.g. SaaS, Fintech)","nullable":true},"websiteUrl":{"type":"string","description":"Organization website URL","nullable":true}},"description":"Organization account details"},"SingleValueResponseOrganizationDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OrganizationDto"}}},"UpdateOnboardingStageRequest":{"required":["stage"],"type":"object","properties":{"stage":{"type":"string","description":"New onboarding stage","enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]}},"description":"Advance the user's onboarding stage"},"SingleValueResponseUserDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UserDto"}}},"UserDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique user identifier","format":"int32"},"email":{"type":"string","description":"User email address"},"emailVerified":{"type":"boolean","description":"Whether the email address has been verified"},"name":{"type":"string","description":"Display name; null if not set","nullable":true},"userRole":{"type":"string","description":"Platform role: USER or SUPERADMIN","enum":["SUPERADMIN","ADMIN","USER"]},"onboardingStage":{"type":"string","description":"Current onboarding progress stage; null when completed","nullable":true,"enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]},"imageUrl":{"type":"string","description":"Profile image URL; null if not set","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the account was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the account was last updated","format":"date-time"}},"description":"User account details"},"UpdateProfileRequest":{"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New display name (max 200 chars)"}}},"UpdateNotificationPreferencesRequest":{"required":["preferences"],"type":"object","properties":{"preferences":{"type":"object","additionalProperties":{"type":"boolean","description":"Map of category keys to enabled/disabled flags"},"description":"Map of category keys to enabled/disabled flags"}},"description":"Replace notification preferences for the current user"},"NotificationPreferencesDto":{"type":"object","properties":{"preferences":{"type":"object","additionalProperties":{"type":"boolean","description":"Map of category keys to enabled/disabled flags"},"description":"Map of category keys to enabled/disabled flags"},"updatedAt":{"type":"string","description":"Timestamp when preferences were last updated","format":"date-time"}},"description":"User notification preferences keyed by notification category"},"SingleValueResponseNotificationPreferencesDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationPreferencesDto"}}},"UpdateWorkspaceRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New workspace name"}},"description":"Update workspace details"},"SingleValueResponseWorkspaceDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WorkspaceDto"}}},"WorkspaceDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique workspace identifier","format":"int32"},"createdAt":{"type":"string","description":"Timestamp when the workspace was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the workspace was last updated","format":"date-time"},"name":{"type":"string","description":"Workspace name"},"orgId":{"type":"integer","description":"Organization this workspace belongs to","format":"int32"}},"description":"Workspace within an organization"},"UpdateUserRequest":{"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New display name (max 200 chars)"},"email":{"type":"string","description":"New email address","format":"email"},"userRole":{"type":"string","description":"New platform role","enum":["SUPERADMIN","ADMIN","USER"]},"onboardingStage":{"type":"string","description":"New onboarding stage","enum":["WELCOME","FIRST_MONITOR","SETUP_COMPLETE","COMPLETED"]},"imageUrl":{"maxLength":500,"minLength":0,"type":"string","description":"New profile image URL (max 500 chars)"}}},"ChangeRoleRequest":{"required":["orgRole"],"type":"object","properties":{"orgRole":{"type":"string","description":"New role to assign","enum":["OWNER","ADMIN","MEMBER"]}},"description":"Update an organization member's role"},"UpdateWebhookEndpointRequest":{"type":"object","properties":{"url":{"maxLength":2048,"minLength":0,"type":"string","description":"New webhook URL; null preserves current","nullable":true},"description":{"maxLength":255,"minLength":0,"type":"string","description":"New description; null preserves current","nullable":true},"subscribedEvents":{"type":"array","description":"Replace subscribed events; null preserves current","nullable":true,"items":{"type":"string","description":"Replace subscribed events; null preserves current","nullable":true}},"enabled":{"type":"boolean","description":"Enable or disable delivery; null preserves current","nullable":true}}},"SingleValueResponseWebhookEndpointDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookEndpointDto"}}},"WebhookEndpointDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique webhook endpoint identifier","format":"uuid"},"url":{"type":"string","description":"HTTPS endpoint URL that receives event payloads"},"description":{"type":"string","description":"Human-readable description of this endpoint","nullable":true},"subscribedEvents":{"type":"array","description":"Event types this endpoint is subscribed to","items":{"type":"string","description":"Event types this endpoint is subscribed to"}},"enabled":{"type":"boolean","description":"Whether delivery is enabled for this endpoint"},"consecutiveFailures":{"type":"integer","description":"Number of consecutive delivery failures","format":"int32"},"disabledReason":{"type":"string","description":"Reason the endpoint was auto-disabled","nullable":true},"disabledAt":{"type":"string","description":"Timestamp when the endpoint was auto-disabled","format":"date-time","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the endpoint was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the endpoint was last updated","format":"date-time"}},"description":"Webhook endpoint that receives event delivery payloads"},"UpdateTagRequest":{"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"New tag name","nullable":true},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"New hex color code","nullable":true}},"description":"Request body for updating a tag; null fields are left unchanged"},"SingleValueResponseTagDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TagDto"}}},"TagDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique tag identifier","format":"uuid"},"organizationId":{"type":"integer","description":"Organization this tag belongs to","format":"int32"},"name":{"type":"string","description":"Tag name, unique within the org"},"color":{"type":"string","description":"Hex color code for display (e.g. #6B7280)"},"createdAt":{"type":"string","description":"Timestamp when the tag was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the tag was last updated","format":"date-time"}},"description":"Tag for organizing and filtering monitors"},"UpdateSecretRequest":{"required":["value"],"type":"object","properties":{"value":{"maxLength":32768,"minLength":0,"type":"string","description":"New secret value, stored encrypted (max 32KB)"}}},"MonitorReference":{"type":"object","properties":{"id":{"type":"string","description":"Monitor identifier","format":"uuid"},"name":{"type":"string","description":"Monitor name"}},"description":"Monitors that reference this secret; null on create/update responses"},"SecretDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique secret identifier","format":"uuid"},"key":{"type":"string","description":"Secret key name, unique within the workspace"},"dekVersion":{"type":"integer","description":"DEK version at the time of last encryption","format":"int32"},"valueHash":{"type":"string","description":"SHA-256 hex digest of the current plaintext; use for change detection"},"createdAt":{"type":"string","description":"Timestamp when the secret was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the secret was last updated","format":"date-time"},"usedByMonitors":{"type":"array","description":"Monitors that reference this secret; null on create/update responses","nullable":true,"items":{"$ref":"#/components/schemas/MonitorReference"}}},"description":"Secret with change-detection hash; plaintext value is never returned"},"SingleValueResponseSecretDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SecretDto"}}},"RetryStrategy":{"required":["type"],"type":"object","properties":{"type":{"type":"string","description":"Retry strategy kind, e.g. fixed interval between attempts"},"maxRetries":{"type":"integer","description":"Maximum number of retries after a failed check","format":"int32"},"interval":{"type":"integer","description":"Delay between retry attempts in seconds","format":"int32"}},"description":"Default retry strategy for member monitors; null clears"},"UpdateResourceGroupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this group"},"description":{"type":"string","description":"Optional description; null clears the existing value","nullable":true},"alertPolicyId":{"type":"string","description":"Optional notification policy to apply for this group; null clears the existing value","format":"uuid","nullable":true},"defaultFrequency":{"maximum":86400,"minimum":30,"type":"integer","description":"Default check frequency in seconds for members (30–86400); null clears","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions for member monitors; null clears","nullable":true,"items":{"type":"string","description":"Default regions for member monitors; null clears","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs for member monitors; null clears","nullable":true,"items":{"type":"string","description":"Default alert channel IDs for member monitors; null clears","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID for member monitors; null clears","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE; null disables threshold","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"maximum":100,"exclusiveMaximum":false,"minimum":0,"exclusiveMinimum":false,"type":"number","description":"Health threshold value; null disables threshold","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"Suppress member-level alert notifications; null preserves current value","nullable":true},"confirmationDelaySeconds":{"maximum":600,"minimum":0,"type":"integer","description":"Confirmation delay in seconds; null clears","format":"int32","nullable":true},"recoveryCooldownMinutes":{"maximum":60,"minimum":0,"type":"integer","description":"Recovery cooldown in minutes; null clears","format":"int32","nullable":true}},"description":"Request body for updating a resource group"},"ResourceGroupDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique resource group identifier","format":"uuid"},"organizationId":{"type":"integer","description":"Organization this group belongs to","format":"int32"},"name":{"type":"string","description":"Human-readable group name"},"slug":{"type":"string","description":"URL-safe group identifier"},"description":{"type":"string","description":"Optional group description","nullable":true},"alertPolicyId":{"type":"string","description":"Notification policy applied to this group","format":"uuid","nullable":true},"defaultFrequency":{"type":"integer","description":"Default check frequency in seconds for member monitors","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions for member monitors","nullable":true,"items":{"type":"string","description":"Default regions for member monitors","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs for member monitors","nullable":true,"items":{"type":"string","description":"Default alert channel IDs for member monitors","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID for member monitors","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"type":"number","description":"Health threshold value","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"When true, member-level incidents skip notification dispatch; only group alerts fire"},"confirmationDelaySeconds":{"type":"integer","description":"Seconds to wait after health threshold breach before creating group incident","format":"int32","nullable":true},"recoveryCooldownMinutes":{"type":"integer","description":"Cooldown minutes after group incident resolves before a new one can open","format":"int32","nullable":true},"health":{"$ref":"#/components/schemas/ResourceGroupHealthDto"},"members":{"type":"array","description":"Member list with individual statuses; populated on detail GET only","nullable":true,"items":{"$ref":"#/components/schemas/ResourceGroupMemberDto"}},"createdAt":{"type":"string","description":"Timestamp when the group was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the group was last updated","format":"date-time"}},"description":"Resource group with health summary and optional member details"},"ResourceGroupHealthDto":{"type":"object","properties":{"status":{"type":"string","description":"Worst-of health status across all members","enum":["operational","maintenance","degraded","down"]},"totalMembers":{"type":"integer","description":"Total number of members in the group","format":"int32"},"operationalCount":{"type":"integer","description":"Number of members currently in operational status","format":"int32"},"activeIncidents":{"type":"integer","description":"Number of members with an active incident or non-operational status","format":"int32"},"thresholdStatus":{"type":"string","description":"Computed group health status based on threshold: 'healthy', 'degraded', or 'down'. Null when no health threshold is configured.","nullable":true,"enum":["healthy","degraded","down"]},"failingCount":{"type":"integer","description":"Number of failing members at time of last evaluation","format":"int32","nullable":true}},"description":"Aggregated health summary for a resource group"},"ResourceGroupMemberDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique group member record identifier","format":"uuid"},"groupId":{"type":"string","description":"Resource group this member belongs to","format":"uuid"},"memberType":{"type":"string","description":"Type of member: 'monitor' or 'service'"},"monitorId":{"type":"string","description":"Monitor ID; set when memberType is 'monitor'","format":"uuid","nullable":true},"serviceId":{"type":"string","description":"Service ID; set when memberType is 'service'","format":"uuid","nullable":true},"name":{"type":"string","description":"Display name of the referenced monitor or service","nullable":true},"slug":{"type":"string","description":"Slug identifier for the service (services only); used for icons and uptime API calls","nullable":true},"subscriptionId":{"type":"string","description":"Subscription ID for the service (services only); used to link to the dependency detail page","format":"uuid","nullable":true},"status":{"type":"string","description":"Computed health status for this member","enum":["operational","maintenance","degraded","down"]},"effectiveFrequency":{"type":"string","description":"Effective check frequency label showing the group default when the monitor inherits it; null for services or when no group default is configured","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the member was added to the group","format":"date-time"},"uptime24h":{"type":"number","description":"24h uptime percentage; populated when includeMetrics=true","format":"double","nullable":true},"chartData":{"type":"array","description":"Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true","nullable":true,"items":{"type":"number","description":"Uptime tick values (0-100) for last-24h mini chart; populated when includeMetrics=true","format":"double","nullable":true}},"avgLatencyMs":{"type":"number","description":"Average latency in ms (monitors only); populated when includeMetrics=true","format":"double","nullable":true},"p95LatencyMs":{"type":"number","description":"P95 latency in ms (monitors only); populated when includeMetrics=true","format":"double","nullable":true},"lastCheckedAt":{"type":"string","description":"Timestamp of the most recent health check; populated when includeMetrics=true","format":"date-time","nullable":true},"monitorType":{"type":"string","description":"Monitor type (HTTP, DNS, TCP, ICMP, HEARTBEAT, MCP); monitors only","nullable":true},"environmentName":{"type":"string","description":"Environment name; monitors only","nullable":true}},"description":"A single member of a resource group with its computed health status"},"SingleValueResponseResourceGroupDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupDto"}}},"EscalationChain":{"required":["steps"],"type":"object","properties":{"steps":{"minItems":1,"type":"array","description":"Ordered escalation steps, evaluated in sequence","items":{"$ref":"#/components/schemas/EscalationStep"}},"onResolve":{"type":"string","description":"Action when the incident resolves","nullable":true},"onReopen":{"type":"string","description":"Action when a resolved incident reopens","nullable":true}},"description":"Escalation chain defining which channels to notify; null preserves current"},"EscalationStep":{"required":["channelIds"],"type":"object","properties":{"delayMinutes":{"minimum":0,"type":"integer","description":"Minutes to wait before executing this step (0 = immediate)","format":"int32"},"channelIds":{"minItems":1,"type":"array","description":"Alert channel IDs to notify in this step","items":{"type":"string","description":"Alert channel IDs to notify in this step","format":"uuid"}},"requireAck":{"type":"boolean","description":"Whether an acknowledgment is required before escalating","nullable":true},"repeatIntervalSeconds":{"minimum":1,"type":"integer","description":"Repeat notification interval in seconds until acknowledged","format":"int32","nullable":true}},"description":"Ordered escalation steps, evaluated in sequence"},"MatchRule":{"required":["type"],"type":"object","properties":{"type":{"type":"string","description":"Rule type, e.g. severity_gte, monitor_id_in, region_in"},"value":{"type":"string","description":"Comparison value for single-value rules like severity_gte","nullable":true},"monitorIds":{"type":"array","description":"Monitor UUIDs to match for monitor_id_in rules","nullable":true,"items":{"type":"string","description":"Monitor UUIDs to match for monitor_id_in rules","format":"uuid","nullable":true}},"regions":{"type":"array","description":"Region codes to match for region_in rules","nullable":true,"items":{"type":"string","description":"Region codes to match for region_in rules","nullable":true}},"values":{"type":"array","description":"Values list for multi-value rules like monitor_type_in","nullable":true,"items":{"type":"string","description":"Values list for multi-value rules like monitor_type_in","nullable":true}}},"description":"Match rules to evaluate (all must pass; omit or empty for catch-all)"},"UpdateNotificationPolicyRequest":{"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this policy; null preserves current"},"matchRules":{"type":"array","description":"Match rules to evaluate (all must pass; omit or empty for catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is enabled; null preserves current"},"priority":{"type":"integer","description":"Evaluation priority; higher value = evaluated first; null preserves current","format":"int32"}},"description":"Request body for updating a notification policy (null fields are preserved)"},"NotificationPolicyDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique notification policy identifier","format":"uuid"},"organizationId":{"type":"integer","description":"Organization this policy belongs to","format":"int32"},"name":{"type":"string","description":"Human-readable name for this policy"},"matchRules":{"type":"array","description":"Match rules (all must pass; empty = catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is active"},"priority":{"type":"integer","description":"Evaluation order; higher value = evaluated first","format":"int32"},"createdAt":{"type":"string","description":"Timestamp when the policy was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the policy was last updated","format":"date-time"}},"description":"Org-level notification policy with match rules and escalation chain"},"SingleValueResponseNotificationPolicyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationPolicyDto"}}},"ConfirmationPolicy":{"required":["type"],"type":"object","properties":{"type":{"type":"string","description":"How incident confirmation is coordinated across regions","enum":["multi_region"]},"minRegionsFailing":{"type":"integer","description":"Minimum failing regions required to confirm an incident","format":"int32"},"maxWaitSeconds":{"type":"integer","description":"Maximum seconds to wait for enough regions to fail after first trigger","format":"int32"}},"description":"Multi-region confirmation settings"},"IncidentPolicyDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique incident policy identifier","format":"uuid"},"monitorId":{"type":"string","description":"Monitor this policy is attached to","format":"uuid"},"triggerRules":{"type":"array","description":"Array of trigger rules defining when an incident should be raised","items":{"$ref":"#/components/schemas/TriggerRule"}},"confirmation":{"$ref":"#/components/schemas/ConfirmationPolicy"},"recovery":{"$ref":"#/components/schemas/RecoveryPolicy"},"createdAt":{"type":"string","description":"Timestamp when the policy was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the policy was last updated","format":"date-time"},"monitorRegionCount":{"type":"integer","description":"Number of regions configured on the monitor (only set in internal API responses)","format":"int32","nullable":true},"checkFrequencySeconds":{"type":"integer","description":"Monitor check frequency in seconds (only set in internal API responses)","format":"int32","nullable":true}},"description":"Incident detection, confirmation, and recovery policy for a monitor"},"RecoveryPolicy":{"type":"object","properties":{"consecutiveSuccesses":{"type":"integer","description":"Consecutive passing checks required to auto-resolve the incident","format":"int32"},"minRegionsPassing":{"type":"integer","description":"Minimum regions that must be passing before recovery can complete","format":"int32"},"cooldownMinutes":{"type":"integer","description":"Minutes after resolve before a new incident may open on the same monitor","format":"int32"}},"description":"Auto-recovery settings"},"TriggerRule":{"required":["scope","severity","type"],"type":"object","properties":{"type":{"type":"string","description":"Condition that opens or escalates an incident from check results","enum":["consecutive_failures","failures_in_window","response_time"]},"count":{"type":"integer","description":"Failure count for consecutive or windowed failure rules","format":"int32","nullable":true},"windowMinutes":{"type":"integer","description":"Window length in minutes for failures-in-window rules","format":"int32","nullable":true},"scope":{"type":"string","description":"Whether the rule applies per region or across regions","nullable":true,"enum":["per_region","any_region"]},"thresholdMs":{"type":"integer","description":"Response time threshold in milliseconds for response-time rules","format":"int32","nullable":true},"severity":{"type":"string","description":"Incident severity when this rule fires","enum":["down","degraded"]},"aggregationType":{"type":"string","description":"How response times are aggregated for response-time rules","nullable":true,"enum":["all_exceed","average","p95","max"]}},"description":"Array of trigger rules defining when an incident should be raised"},"UpdateIncidentPolicyRequest":{"required":["confirmation","recovery","triggerRules"],"type":"object","properties":{"triggerRules":{"minItems":1,"type":"array","description":"Array of trigger rules; at least one required","items":{"$ref":"#/components/schemas/TriggerRule"}},"confirmation":{"$ref":"#/components/schemas/ConfirmationPolicy"},"recovery":{"$ref":"#/components/schemas/RecoveryPolicy"}},"description":"Request body for updating an incident policy"},"SingleValueResponseIncidentPolicyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentPolicyDto"}}},"ApiKeyAuthConfig":{"required":["headerName"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"pattern":"^[A-Za-z0-9\\-_]+$","type":"string","description":"HTTP header name that carries the API key"},"vaultSecretId":{"type":"string","description":"Vault secret ID for the API key value","format":"uuid","nullable":true}}}]},"BasicAuthConfig":{"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"vaultSecretId":{"type":"string","description":"Vault secret ID holding Basic auth username and password","format":"uuid","nullable":true}}}]},"BearerAuthConfig":{"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"vaultSecretId":{"type":"string","description":"Vault secret ID holding the bearer token value","format":"uuid","nullable":true}}}]},"HeaderAuthConfig":{"required":["headerName"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorAuthConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"pattern":"^[A-Za-z0-9\\-_]+$","type":"string","description":"Custom HTTP header name for the secret value"},"vaultSecretId":{"type":"string","description":"Vault secret ID for the header value","format":"uuid","nullable":true}}}]},"MonitorAuthConfig":{"required":["type"],"type":"object","properties":{"type":{"type":"string"}},"description":"New authentication configuration (full replacement)","discriminator":{"propertyName":"type"}},"UpdateMonitorAuthRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"MonitorAuthDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"authType":{"type":"string","enum":["bearer","basic","header","api_key"]},"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"SingleValueResponseMonitorAuthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorAuthDto"}}},"AssertionConfig":{"required":["type"],"type":"object","properties":{"type":{"type":"string"}},"description":"New assertion configuration (full replacement)","discriminator":{"propertyName":"type"}},"BodyContainsAssertion":{"required":["substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"substring":{"minLength":1,"type":"string","description":"Substring that must appear in the response body"}}}]},"DnsExpectedCnameAssertion":{"required":["value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"value":{"minLength":1,"type":"string","description":"Expected CNAME target the resolution must include"}}}]},"DnsExpectedIpsAssertion":{"required":["ips"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"ips":{"minItems":1,"type":"array","description":"Allowed IP addresses; at least one resolved address must match","items":{"type":"string","description":"Allowed IP addresses; at least one resolved address must match"}}}}]},"DnsMaxAnswersAssertion":{"required":["recordType"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string","description":"DNS record type whose answer count is checked"},"max":{"type":"integer","description":"Maximum number of answers allowed for that record type","format":"int32"}}}]},"DnsMinAnswersAssertion":{"required":["recordType"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string","description":"DNS record type whose answer count is checked"},"min":{"type":"integer","description":"Minimum number of answers required for that record type","format":"int32"}}}]},"DnsRecordContainsAssertion":{"required":["recordType","substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string","description":"DNS record type to assert on (A, AAAA, CNAME, MX, TXT)"},"substring":{"minLength":1,"type":"string","description":"Substring that must appear in a matching record value"}}}]},"DnsRecordEqualsAssertion":{"required":["recordType","value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"recordType":{"minLength":1,"type":"string","description":"DNS record type to assert on (A, AAAA, CNAME, MX, TXT)"},"value":{"minLength":1,"type":"string","description":"Expected DNS record value for an exact match"}}}]},"DnsResolvesAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"DnsResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","description":"Maximum allowed DNS resolution time in milliseconds","format":"int32"}}}]},"DnsResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","description":"DNS resolution time in milliseconds that triggers a warning only","format":"int32"}}}]},"DnsTtlHighAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxTtl":{"type":"integer","description":"Maximum TTL in seconds before a high-TTL warning is raised","format":"int32"}}}]},"DnsTtlLowAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"minTtl":{"type":"integer","description":"Minimum acceptable TTL in seconds before a warning is raised","format":"int32"}}}]},"DnsTxtContainsAssertion":{"required":["substring"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"substring":{"minLength":1,"type":"string","description":"Substring that must appear in at least one TXT record"}}}]},"HeaderValueAssertion":{"required":["expected","headerName","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"headerName":{"minLength":1,"type":"string","description":"HTTP header name to assert on"},"expected":{"minLength":1,"type":"string","description":"Expected value to compare against"},"operator":{"type":"string","description":"Comparison operator (equals, contains, less_than, greater_than, etc.)","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"HeartbeatIntervalDriftAssertion":{"required":["maxDeviationPercent"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxDeviationPercent":{"maximum":100,"minimum":1,"type":"integer","description":"Max percent drift from expected ping interval before warning (non-fatal)","format":"int32"}}}]},"HeartbeatMaxIntervalAssertion":{"required":["maxSeconds"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxSeconds":{"minimum":1,"type":"integer","description":"Maximum allowed gap in seconds between consecutive heartbeat pings","format":"int32"}}}]},"HeartbeatPayloadContainsAssertion":{"required":["path","value"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"path":{"minLength":1,"type":"string","description":"JSONPath expression into the heartbeat ping JSON payload"},"value":{"type":"string","description":"Expected value to compare against at that path"}}}]},"HeartbeatReceivedAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"IcmpPacketLossAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxPercent":{"maximum":100.0,"exclusiveMaximum":false,"minimum":0.0,"exclusiveMinimum":false,"type":"number","description":"Maximum allowed packet loss percentage before the check fails (0–100)","format":"double"}}}]},"IcmpReachableAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"IcmpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","description":"Maximum average ICMP round-trip time in milliseconds","format":"int32"}}}]},"IcmpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","description":"ICMP round-trip time in milliseconds that triggers a warning only","format":"int32"}}}]},"JsonPathAssertion":{"required":["expected","operator","path"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"path":{"minLength":1,"type":"string","description":"JSONPath expression to extract a value from the response body"},"expected":{"minLength":1,"type":"string","description":"Expected value to compare against"},"operator":{"type":"string","description":"Comparison operator (equals, contains, less_than, greater_than, etc.)","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"McpConnectsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"McpHasCapabilityAssertion":{"required":["capability"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"capability":{"minLength":1,"type":"string","description":"Capability name the server must advertise, e.g. tools or resources"}}}]},"McpMinToolsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"min":{"type":"integer","description":"Minimum number of tools the server must expose","format":"int32"}}}]},"McpProtocolVersionAssertion":{"required":["version"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"version":{"minLength":1,"type":"string","description":"Expected MCP protocol version string from the server handshake"}}}]},"McpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","description":"Maximum allowed MCP check duration in milliseconds","format":"int32"}}}]},"McpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","description":"MCP check duration in milliseconds that triggers a warning only","format":"int32"}}}]},"McpToolAvailableAssertion":{"required":["toolName"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"toolName":{"minLength":1,"type":"string","description":"MCP tool name that must appear in the server's tool list"}}}]},"McpToolCountChangedAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expectedCount":{"type":"integer","description":"Expected tool count; warns when the live count differs","format":"int32"}}}]},"RedirectCountAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxCount":{"type":"integer","description":"Maximum number of HTTP redirects allowed before the check fails","format":"int32"}}}]},"RedirectTargetAssertion":{"required":["expected","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expected":{"minLength":1,"type":"string","description":"Expected final URL after following redirects"},"operator":{"type":"string","description":"Comparison operator (equals, contains, less_than, greater_than, etc.)","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"RegexBodyAssertion":{"required":["pattern"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"pattern":{"minLength":1,"type":"string","description":"Regular expression the response body must match"}}}]},"ResponseSizeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxBytes":{"type":"integer","description":"Maximum response body size in bytes before the check fails","format":"int32"}}}]},"ResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"thresholdMs":{"type":"integer","description":"Maximum allowed response time in milliseconds before the check fails","format":"int32"}}}]},"ResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","description":"HTTP response time in milliseconds that triggers a warning only","format":"int32"}}}]},"SslExpiryAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"minDaysRemaining":{"type":"integer","description":"Minimum days before TLS certificate expiry; fails or warns below this threshold","format":"int32"}}}]},"StatusCodeAssertion":{"required":["expected","operator"],"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"expected":{"minLength":1,"type":"string","description":"Expected status code, range pattern, or wildcard such as 2xx"},"operator":{"type":"string","description":"Comparison operator (equals, contains, less_than, greater_than, etc.)","enum":["equals","contains","less_than","greater_than","matches","range"]}}}]},"TcpConnectsAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"}]},"TcpResponseTimeAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"maxMs":{"type":"integer","description":"Maximum TCP connect time in milliseconds before the check fails","format":"int32"}}}]},"TcpResponseTimeWarnAssertion":{"type":"object","allOf":[{"$ref":"#/components/schemas/AssertionConfig"},{"type":"object","properties":{"warnMs":{"type":"integer","description":"TCP connect time in milliseconds that triggers a warning only","format":"int32"}}}]},"UpdateAssertionRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","description":"New outcome severity: FAIL or WARN","enum":["fail","warn"]}}},"MonitorAssertionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"monitorId":{"type":"string","format":"uuid"},"assertionType":{"type":"string","enum":["status_code","response_time","body_contains","json_path","header","regex","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":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","enum":["fail","warn"]}}},"SingleValueResponseMonitorAssertionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorAssertionDto"}}},"SetAlertChannelsRequest":{"required":["channelIds"],"type":"object","properties":{"channelIds":{"type":"array","description":"IDs of alert channels to link (replaces current list)","items":{"type":"string","description":"IDs of alert channels to link (replaces current list)","format":"uuid"}}},"description":"Replace the alert channels linked to a monitor"},"SingleValueResponseListUUID":{"type":"object","properties":{"data":{"type":"array","nullable":true,"items":{"type":"string","format":"uuid","nullable":true}}}},"AddMonitorTagsRequest":{"type":"object","properties":{"tagIds":{"type":"array","description":"IDs of existing org tags to attach","nullable":true,"items":{"type":"string","description":"IDs of existing org tags to attach","format":"uuid","nullable":true}},"newTags":{"type":"array","description":"New tags to create (if not already present) and attach","nullable":true,"items":{"$ref":"#/components/schemas/NewTagRequest"}}},"description":"Request body for adding tags to a monitor. Provide existing tag IDs, inline new tags, or both."},"CreateAssertionRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/BodyContainsAssertion"},{"$ref":"#/components/schemas/DnsExpectedCnameAssertion"},{"$ref":"#/components/schemas/DnsExpectedIpsAssertion"},{"$ref":"#/components/schemas/DnsMaxAnswersAssertion"},{"$ref":"#/components/schemas/DnsMinAnswersAssertion"},{"$ref":"#/components/schemas/DnsRecordContainsAssertion"},{"$ref":"#/components/schemas/DnsRecordEqualsAssertion"},{"$ref":"#/components/schemas/DnsResolvesAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeAssertion"},{"$ref":"#/components/schemas/DnsResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/DnsTtlHighAssertion"},{"$ref":"#/components/schemas/DnsTtlLowAssertion"},{"$ref":"#/components/schemas/DnsTxtContainsAssertion"},{"$ref":"#/components/schemas/HeaderValueAssertion"},{"$ref":"#/components/schemas/HeartbeatIntervalDriftAssertion"},{"$ref":"#/components/schemas/HeartbeatMaxIntervalAssertion"},{"$ref":"#/components/schemas/HeartbeatPayloadContainsAssertion"},{"$ref":"#/components/schemas/HeartbeatReceivedAssertion"},{"$ref":"#/components/schemas/IcmpPacketLossAssertion"},{"$ref":"#/components/schemas/IcmpReachableAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeAssertion"},{"$ref":"#/components/schemas/IcmpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/JsonPathAssertion"},{"$ref":"#/components/schemas/McpConnectsAssertion"},{"$ref":"#/components/schemas/McpHasCapabilityAssertion"},{"$ref":"#/components/schemas/McpMinToolsAssertion"},{"$ref":"#/components/schemas/McpProtocolVersionAssertion"},{"$ref":"#/components/schemas/McpResponseTimeAssertion"},{"$ref":"#/components/schemas/McpResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/McpToolAvailableAssertion"},{"$ref":"#/components/schemas/McpToolCountChangedAssertion"},{"$ref":"#/components/schemas/RedirectCountAssertion"},{"$ref":"#/components/schemas/RedirectTargetAssertion"},{"$ref":"#/components/schemas/RegexBodyAssertion"},{"$ref":"#/components/schemas/ResponseSizeAssertion"},{"$ref":"#/components/schemas/ResponseTimeAssertion"},{"$ref":"#/components/schemas/ResponseTimeWarnAssertion"},{"$ref":"#/components/schemas/SslExpiryAssertion"},{"$ref":"#/components/schemas/StatusCodeAssertion"},{"$ref":"#/components/schemas/TcpConnectsAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeAssertion"},{"$ref":"#/components/schemas/TcpResponseTimeWarnAssertion"}]},"severity":{"type":"string","description":"Outcome severity: FAIL (fails the check) or WARN (warns without failing)","enum":["fail","warn"]}},"description":"Replace all assertions; null preserves current"},"DnsMonitorConfig":{"required":["hostname"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"hostname":{"minLength":1,"type":"string","description":"Domain name to resolve"},"recordTypes":{"type":"array","description":"DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR","nullable":true,"items":{"type":"string","description":"DNS record types to query: A, AAAA, CNAME, MX, NS, TXT, SRV, SOA, CAA, PTR","nullable":true,"enum":["A","AAAA","CNAME","MX","NS","TXT","SRV","SOA","CAA","PTR"]}},"nameservers":{"type":"array","description":"Custom nameservers to query (uses system defaults if omitted)","nullable":true,"items":{"type":"string","description":"Custom nameservers to query (uses system defaults if omitted)","nullable":true}},"timeoutMs":{"type":"integer","description":"Per-query timeout in milliseconds","format":"int32","nullable":true},"totalTimeoutMs":{"type":"integer","description":"Total timeout for all queries in milliseconds","format":"int32","nullable":true}}}]},"HeartbeatMonitorConfig":{"required":["expectedInterval","gracePeriod"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"expectedInterval":{"maximum":86400,"minimum":1,"type":"integer","description":"Expected heartbeat interval in seconds","format":"int32"},"gracePeriod":{"minimum":1,"type":"integer","description":"Grace period in seconds before marking as down","format":"int32"}}}]},"HttpMonitorConfig":{"required":["method","url"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Target URL to send requests to"},"method":{"type":"string","description":"HTTP method: GET, POST, PUT, PATCH, DELETE, or HEAD","enum":["GET","POST","PUT","PATCH","DELETE","HEAD"]},"customHeaders":{"type":"object","additionalProperties":{"type":"string","description":"Additional HTTP headers to include in requests","nullable":true},"description":"Additional HTTP headers to include in requests","nullable":true},"requestBody":{"type":"string","description":"Request body content for POST/PUT/PATCH methods","nullable":true},"contentType":{"type":"string","description":"Content-Type header value for the request body","nullable":true},"verifyTls":{"type":"boolean","description":"Whether to verify TLS certificates (default: true)","nullable":true}}}]},"IcmpMonitorConfig":{"required":["host"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"host":{"minLength":1,"type":"string","description":"Target hostname or IP address to ping"},"packetCount":{"maximum":20,"minimum":1,"type":"integer","description":"Number of ICMP packets to send","format":"int32","nullable":true},"timeoutMs":{"type":"integer","description":"Ping timeout in milliseconds","format":"int32","nullable":true}}}]},"McpServerMonitorConfig":{"required":["command"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"command":{"minLength":1,"type":"string","description":"Command to execute to start the MCP server"},"args":{"type":"array","description":"Command-line arguments for the MCP server process","nullable":true,"items":{"type":"string","description":"Command-line arguments for the MCP server process","nullable":true}},"env":{"type":"object","additionalProperties":{"type":"string","description":"Environment variables to pass to the MCP server process","nullable":true},"description":"Environment variables to pass to the MCP server process","nullable":true}}}]},"MonitorConfig":{"type":"object","description":"Updated protocol-specific configuration; null preserves current"},"NewTagRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Tag name"},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"Hex color code (defaults to #6B7280 if omitted)","nullable":true}},"description":"Inline tag creation — creates the tag if it does not already exist"},"TcpMonitorConfig":{"required":["host"],"type":"object","allOf":[{"$ref":"#/components/schemas/MonitorConfig"},{"type":"object","properties":{"host":{"minLength":1,"type":"string","description":"Target hostname or IP address"},"port":{"maximum":65535,"minimum":1,"type":"integer","description":"TCP port to connect to","format":"int32"},"timeoutMs":{"type":"integer","description":"Connection timeout in milliseconds","format":"int32","nullable":true}}}]},"UpdateMonitorRequest":{"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"New monitor name; null preserves current","nullable":true},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"New check frequency in seconds (30–86400); null preserves current","format":"int32","nullable":true},"enabled":{"type":"boolean","description":"Enable or disable the monitor; null preserves current","nullable":true},"regions":{"type":"array","description":"New probe regions; null preserves current","nullable":true,"items":{"type":"string","description":"New probe regions; null preserves current","nullable":true}},"managedBy":{"type":"string","description":"New management source; null preserves current","nullable":true,"enum":["DASHBOARD","CLI"]},"environmentId":{"type":"string","description":"New environment ID; null preserves current (use clearEnvironmentId to unset)","format":"uuid","nullable":true},"clearEnvironmentId":{"type":"boolean","description":"Set to true to remove the environment association","nullable":true},"assertions":{"type":"array","description":"Replace all assertions; null preserves current","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}},"auth":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]},"clearAuth":{"type":"boolean","description":"Set to true to remove authentication","nullable":true},"incidentPolicy":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"},"alertChannelIds":{"type":"array","description":"Replace alert channel list; null preserves current","nullable":true,"items":{"type":"string","description":"Replace alert channel list; null preserves current","format":"uuid","nullable":true}},"tags":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"MonitorDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique monitor identifier","format":"uuid"},"organizationId":{"type":"integer","description":"Organization this monitor belongs to","format":"int32"},"name":{"type":"string","description":"Human-readable name for this monitor"},"type":{"type":"string","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"Check frequency in seconds (30–86400)","format":"int32"},"enabled":{"type":"boolean","description":"Whether the monitor is active"},"regions":{"type":"array","description":"Probe regions where checks are executed","items":{"type":"string","description":"Probe regions where checks are executed"}},"managedBy":{"type":"string","description":"Management source: DASHBOARD or CLI","enum":["DASHBOARD","CLI"]},"createdAt":{"type":"string","description":"Timestamp when the monitor was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the monitor was last updated","format":"date-time"},"assertions":{"type":"array","description":"Assertions evaluated against each check result; null on list responses","nullable":true,"items":{"$ref":"#/components/schemas/MonitorAssertionDto"}},"tags":{"type":"array","description":"Tags applied to this monitor","nullable":true,"items":{"$ref":"#/components/schemas/TagDto"}},"pingUrl":{"type":"string","description":"Heartbeat ping URL; populated for HEARTBEAT monitors only","nullable":true},"environment":{"$ref":"#/components/schemas/Summary"},"auth":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]},"incidentPolicy":{"$ref":"#/components/schemas/IncidentPolicyDto"},"alertChannelIds":{"type":"array","description":"Alert channel IDs linked to this monitor; populated on single-monitor responses","nullable":true,"items":{"type":"string","description":"Alert channel IDs linked to this monitor; populated on single-monitor responses","format":"uuid","nullable":true}}},"description":"Full monitor representation"},"SingleValueResponseMonitorDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorDto"}}},"Summary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"}},"description":"Environment associated with this monitor; null when unassigned"},"ChangeStatusRequest":{"required":["status"],"type":"object","properties":{"status":{"type":"string","description":"New membership status (ACTIVE or SUSPENDED)","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}},"description":"Update an organization member's status"},"UpdateMaintenanceWindowRequest":{"required":["endsAt","startsAt"],"type":"object","properties":{"monitorId":{"type":"string","description":"Monitor to attach this maintenance window to; null preserves current","format":"uuid"},"startsAt":{"type":"string","description":"Updated start time (ISO 8601)","format":"date-time"},"endsAt":{"type":"string","description":"Updated end time (ISO 8601)","format":"date-time"},"repeatRule":{"maxLength":100,"minLength":0,"type":"string","description":"Updated iCal RRULE; null clears the repeat rule"},"reason":{"type":"string","description":"Updated reason; null clears the existing reason"},"suppressAlerts":{"type":"boolean","description":"Whether to suppress alerts; null preserves current"}}},"MaintenanceWindowDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique maintenance window identifier","format":"uuid"},"monitorId":{"type":"string","description":"Monitor this window applies to; null for org-wide windows","format":"uuid","nullable":true},"organizationId":{"type":"integer","description":"Organization this maintenance window belongs to","format":"int32"},"startsAt":{"type":"string","description":"Scheduled start of the maintenance window","format":"date-time"},"endsAt":{"type":"string","description":"Scheduled end of the maintenance window","format":"date-time"},"repeatRule":{"type":"string","description":"iCal RRULE for recurring windows; null for one-time","nullable":true},"reason":{"type":"string","description":"Human-readable reason for the maintenance","nullable":true},"suppressAlerts":{"type":"boolean","description":"Whether alerts are suppressed during this window"},"createdAt":{"type":"string","description":"Timestamp when the window was created","format":"date-time"}},"description":"Scheduled maintenance window for a monitor"},"SingleValueResponseMaintenanceWindowDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MaintenanceWindowDto"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"New environment name; null preserves current","nullable":true},"variables":{"type":"object","additionalProperties":{"type":"string","description":"Replace all variables; null preserves current","nullable":true},"description":"Replace all variables; null preserves current","nullable":true},"isDefault":{"type":"boolean","description":"Whether this is the default environment; null preserves current","nullable":true}}},"EnvironmentDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique environment identifier","format":"uuid"},"orgId":{"type":"integer","description":"Organization this environment belongs to","format":"int32"},"name":{"type":"string","description":"Human-readable environment name"},"slug":{"type":"string","description":"URL-safe identifier"},"variables":{"type":"object","additionalProperties":{"type":"string","description":"Key-value variable pairs available for interpolation"},"description":"Key-value variable pairs available for interpolation"},"createdAt":{"type":"string","description":"Timestamp when the environment was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the environment was last updated","format":"date-time"},"monitorCount":{"type":"integer","description":"Number of monitors using this environment","format":"int32"},"isDefault":{"type":"boolean","description":"Whether this is the default environment for new monitors"}},"description":"Environment with variable substitutions for monitor configs"},"SingleValueResponseEnvironmentDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EnvironmentDto"}}},"ChannelConfig":{"required":["channelType"],"type":"object","properties":{"channelType":{"type":"string"}},"description":"New channel configuration (full replacement, not partial update)","discriminator":{"propertyName":"channelType"}},"DiscordChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Discord webhook URL"},"mentionRoleId":{"type":"string","description":"Optional Discord role ID to mention in notifications","nullable":true}}}]},"EmailChannelConfig":{"required":["recipients"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"recipients":{"minItems":1,"type":"array","description":"Email addresses to send notifications to","items":{"type":"string","description":"Email addresses to send notifications to","format":"email"}}}}]},"OpsGenieChannelConfig":{"required":["apiKey"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"apiKey":{"minLength":1,"type":"string","description":"OpsGenie API key for alert creation"},"region":{"type":"string","description":"OpsGenie API region: us or eu","nullable":true}}}]},"PagerDutyChannelConfig":{"required":["routingKey"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"routingKey":{"minLength":1,"type":"string","description":"PagerDuty Events API v2 routing (integration) key"},"severityOverride":{"type":"string","description":"Override PagerDuty severity mapping","nullable":true}}}]},"SlackChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Slack incoming webhook URL"},"mentionText":{"type":"string","description":"Optional mention text included in notifications, e.g. @channel","nullable":true}}}]},"TeamsChannelConfig":{"required":["webhookUrl"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"webhookUrl":{"minLength":1,"type":"string","description":"Microsoft Teams incoming webhook URL"}}}]},"UpdateAlertChannelRequest":{"required":["config","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"New channel name (full replacement, not partial update)"},"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}}},"WebhookChannelConfig":{"required":["url"],"type":"object","allOf":[{"$ref":"#/components/schemas/ChannelConfig"},{"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Webhook endpoint URL that receives alert payloads"},"signingSecret":{"type":"string","description":"Optional HMAC signing secret for payload verification","nullable":true},"customHeaders":{"type":"object","additionalProperties":{"type":"string","description":"Additional HTTP headers to include in webhook requests","nullable":true},"description":"Additional HTTP headers to include in webhook requests","nullable":true}}}]},"AlertChannelDto":{"required":["channelType","createdAt","id","name","updatedAt"],"type":"object","properties":{"id":{"type":"string","description":"Unique alert channel identifier","format":"uuid"},"name":{"type":"string","description":"Human-readable channel name"},"channelType":{"type":"string","description":"Channel integration type (e.g. SLACK, PAGERDUTY, EMAIL)","enum":["email","webhook","slack","pagerduty","opsgenie","teams","discord"]},"displayConfig":{"type":"object","additionalProperties":{"type":"object","description":"Non-sensitive display metadata; null for older channels","nullable":true},"description":"Non-sensitive display metadata; null for older channels","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the channel was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the channel was last updated","format":"date-time"},"configHash":{"type":"string","description":"SHA-256 hash of the channel config; use for change detection","nullable":true},"lastDeliveryAt":{"type":"string","description":"Timestamp of the most recent delivery attempt","format":"date-time","nullable":true},"lastDeliveryStatus":{"type":"string","description":"Outcome of the most recent delivery (SUCCESS, FAILED, etc.)","nullable":true}},"description":"Alert channel with non-sensitive configuration metadata"},"SingleValueResponseAlertChannelDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AlertChannelDto"}}},"WorkspaceCreateParams":{"required":["name"],"type":"object","properties":{"organizationId":{"type":"integer","format":"int32"},"name":{"minLength":1,"type":"string"}}},"ServiceIncidentRequest":{"required":["action","externalRef","serviceId","title"],"type":"object","properties":{"serviceId":{"type":"string","format":"uuid"},"externalRef":{"minLength":1,"type":"string"},"severity":{"type":"string","nullable":true},"title":{"minLength":1,"type":"string"},"shortlink":{"type":"string","nullable":true},"affectedComponents":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"serviceIncidentId":{"type":"string","format":"uuid","nullable":true},"action":{"minLength":1,"type":"string"},"statusText":{"type":"string","nullable":true}}},"IncidentDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique incident identifier","format":"uuid"},"monitorId":{"type":"string","description":"Monitor that triggered the incident; null for service or manual incidents","format":"uuid","nullable":true},"organizationId":{"type":"integer","description":"Organization this incident belongs to","format":"int32"},"source":{"type":"string","description":"Incident origin: MONITOR, SERVICE, or MANUAL","enum":["AUTOMATIC","MANUAL","MONITORS","STATUS_DATA","RESOURCE_GROUP"]},"status":{"type":"string","description":"Current lifecycle status (OPEN, RESOLVED, etc.)","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"severity":{"type":"string","description":"Severity level: DOWN, DEGRADED, or MAINTENANCE","enum":["DOWN","DEGRADED","MAINTENANCE"]},"title":{"type":"string","description":"Short summary of the incident; null for auto-generated incidents","nullable":true},"triggeredByRule":{"type":"string","description":"Human-readable description of the trigger rule that fired","nullable":true},"affectedRegions":{"type":"array","description":"Probe regions that observed the failure","items":{"type":"string","description":"Probe regions that observed the failure"}},"reopenCount":{"type":"integer","description":"Number of times this incident has been reopened","format":"int32"},"createdByUserId":{"type":"integer","description":"User who created the incident (manual incidents only)","format":"int32","nullable":true},"statusPageVisible":{"type":"boolean","description":"Whether this incident is visible on the status page"},"serviceIncidentId":{"type":"string","description":"Linked vendor service incident ID; null for monitor incidents","format":"uuid","nullable":true},"serviceId":{"type":"string","description":"Linked service catalog ID; null for monitor incidents","format":"uuid","nullable":true},"externalRef":{"type":"string","description":"External reference ID (e.g. PagerDuty incident ID)","nullable":true},"affectedComponents":{"type":"array","description":"Service components affected by this incident","nullable":true,"items":{"type":"string","description":"Service components affected by this incident","nullable":true}},"shortlink":{"type":"string","description":"Short URL linking to the incident details","nullable":true},"resolutionReason":{"type":"string","description":"How the incident was resolved (AUTO_RECOVERED, MANUAL, etc.)","nullable":true,"enum":["MANUAL","AUTO_RECOVERED","AUTO_RESOLVED"]},"startedAt":{"type":"string","description":"Timestamp when the incident was detected or created","format":"date-time","nullable":true},"confirmedAt":{"type":"string","description":"Timestamp when the incident was confirmed (multi-region confirmation)","format":"date-time","nullable":true},"resolvedAt":{"type":"string","description":"Timestamp when the incident was resolved","format":"date-time","nullable":true},"cooldownUntil":{"type":"string","description":"Cooldown window end; new incidents suppressed until this time","format":"date-time","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the incident record was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the incident was last updated","format":"date-time"},"monitorName":{"type":"string","description":"Name of the associated monitor; populated on list responses","nullable":true},"serviceName":{"type":"string","description":"Name of the associated service; populated on list responses","nullable":true},"serviceSlug":{"type":"string","description":"Slug of the associated service; populated on list responses","nullable":true},"monitorType":{"type":"string","description":"Type of the associated monitor; populated on list responses","nullable":true},"resourceGroupId":{"type":"string","description":"Resource group that owns this incident; null when not group-managed","format":"uuid","nullable":true},"resourceGroupName":{"type":"string","description":"Name of the resource group; populated on list responses","nullable":true}},"description":"Incident triggered by a monitor check failure or manual creation"},"TableValueResultIncidentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IncidentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseInteger":{"type":"object","properties":{"data":{"type":"integer","format":"int32","nullable":true}}},"CreateAutoIncidentRequest":{"required":["monitorId"],"type":"object","properties":{"monitorId":{"type":"string","format":"uuid"},"severity":{"type":"string","nullable":true},"triggeredByRule":{"type":"string","nullable":true},"affectedRegions":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"startedAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseIncidentDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentDto"}}},"ReopenAutoIncidentRequest":{"type":"object","properties":{"affectedRegions":{"type":"array","items":{"type":"string"}},"severity":{"type":"string","nullable":true}}},"AdapterHealthReportRequest":{"required":["serviceId","success"],"type":"object","properties":{"serviceId":{"type":"string","format":"uuid"},"success":{"type":"boolean"},"errorMessage":{"type":"string","nullable":true}}},"AdapterHealthDto":{"type":"object","properties":{"serviceId":{"type":"string","description":"Service this health record belongs to","format":"uuid"},"serviceSlug":{"type":"string","description":"URL-safe service identifier"},"serviceName":{"type":"string","description":"Service name"},"adapterType":{"type":"string","description":"Data source adapter type","nullable":true},"lastSuccessAt":{"type":"string","description":"Timestamp of the last successful poll","format":"date-time","nullable":true},"lastFailureAt":{"type":"string","description":"Timestamp of the last failed poll","format":"date-time","nullable":true},"consecutiveFailures":{"type":"integer","description":"Number of consecutive poll failures","format":"int32"},"lastErrorMessage":{"type":"string","description":"Error message from the most recent failure","nullable":true},"disabledByHealth":{"type":"boolean","description":"Whether the adapter is disabled due to repeated failures"},"updatedAt":{"type":"string","description":"Timestamp when this health record was last updated","format":"date-time"}}},"SingleValueResponseAdapterHealthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdapterHealthDto"}}},"CreateOrgRequest":{"required":["name"],"type":"object","properties":{"name":{"minLength":1,"type":"string","description":"Organization name"},"email":{"type":"string","description":"Billing and contact email address","format":"email","nullable":true}},"description":"Create a new organization"},"SingleValueResponseTransactionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TransactionDto"}}},"TransactionDto":{"type":"object","properties":{"id":{"type":"string","description":"Paddle transaction identifier"},"status":{"type":"string","description":"Transaction status (e.g. completed, pending)","nullable":true},"currencyCode":{"type":"string","description":"ISO 4217 currency code","nullable":true},"invoiceNumber":{"type":"string","description":"Invoice number; null if not invoiced","nullable":true},"billedAt":{"type":"string","description":"Timestamp when the transaction was billed","format":"date-time","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the transaction was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the transaction was last updated","format":"date-time"},"total":{"type":"string","description":"Total amount as a decimal string (including tax)","nullable":true},"subtotal":{"type":"string","description":"Subtotal before tax as a decimal string","nullable":true},"tax":{"type":"string","description":"Tax amount as a decimal string","nullable":true}},"description":"A billing transaction from Paddle"},"QuickMonitorRequest":{"required":["url"],"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Target URL to monitor"},"name":{"type":"string","description":"Human-readable monitor name; defaults to the hostname if omitted","nullable":true},"frequencySeconds":{"type":"integer","description":"Check frequency in seconds (30–86400); defaults to 60","format":"int32","nullable":true}},"description":"Minimal request for creating an HTTP monitor quickly"},"OnboardingSetupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"Organization or team name (max 200 chars)"},"role":{"maxLength":50,"minLength":0,"type":"string","description":"User's role or job title","nullable":true},"teamSize":{"maxLength":50,"minLength":0,"type":"string","description":"Team size range (e.g. 1-10, 11-50)","nullable":true}}},"AnalyzeUrlRequest":{"required":["url"],"type":"object","properties":{"url":{"minLength":1,"type":"string","description":"Target URL to analyze (must be a valid HTTP/HTTPS URL)"}},"description":"URL to analyze for monitor setup suggestions"},"AnalyzeUrlResponse":{"type":"object","properties":{"reachable":{"type":"boolean","description":"Whether the URL responded during analysis"},"responseTimeMs":{"type":"integer","description":"Response time observed during analysis in milliseconds","format":"int64"},"statusCode":{"type":"integer","description":"HTTP status code from the analysis request","format":"int32"},"tlsExpiry":{"type":"string","description":"TLS certificate expiry date; null for non-HTTPS or unavailable","format":"date-time","nullable":true},"tlsDaysRemaining":{"type":"integer","description":"Days until TLS certificate expires; null if not applicable","format":"int32","nullable":true},"contentType":{"type":"string","description":"Response Content-Type header value","nullable":true},"suggestedName":{"type":"string","description":"Suggested monitor name derived from the URL hostname"},"suggestedAssertions":{"type":"array","description":"Recommended assertions based on the URL response","items":{"$ref":"#/components/schemas/SuggestedAssertion"}},"suggestedFrequencySeconds":{"type":"integer","description":"Suggested check frequency in seconds based on the URL","format":"int32"}},"description":"Analysis of a URL with monitor setup suggestions"},"SingleValueResponseAnalyzeUrlResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AnalyzeUrlResponse"}}},"SuggestedAssertion":{"type":"object","properties":{"type":{"type":"string","description":"Assertion type (e.g. status_code, response_time)"},"operator":{"type":"string","description":"Comparison operator (e.g. equals, less_than)"},"value":{"type":"string","description":"Expected value to compare against"}},"description":"Recommended assertions based on the URL response"},"AcceptInviteRequest":{"required":["token"],"type":"object","properties":{"token":{"minLength":1,"type":"string","description":"Invite token from the invitation email"}},"description":"Accept an organization invite using the invite token"},"AcceptInviteDto":{"type":"object","properties":{"orgId":{"type":"integer","description":"Organization the user joined","format":"int32"},"userId":{"type":"integer","description":"User who accepted the invite","format":"int32"},"orgRole":{"type":"string","description":"Role assigned to the new member","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","description":"Initial membership status after joining","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}},"description":"Result of accepting an organization invite"},"SingleValueResponseAcceptInviteDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AcceptInviteDto"}}},"RegisterUserRequest":{"type":"object","properties":{"nickname":{"type":"string","description":"User nickname from the identity provider","nullable":true},"name":{"type":"string","description":"User display name from the identity provider","nullable":true},"picture":{"type":"string","description":"Profile picture URL from the identity provider","nullable":true}}},"CreateWorkspaceRequest":{"required":["name"],"type":"object","properties":{"name":{"minLength":1,"type":"string","description":"Workspace name"}},"description":"Create a new workspace within the organization"},"AddMemberRequest":{"required":["orgRole","userId"],"type":"object","properties":{"userId":{"type":"integer","description":"ID of the user to add","format":"int32"},"orgRole":{"type":"string","description":"Role to assign to the new member","enum":["OWNER","ADMIN","MEMBER"]}},"description":"Add an existing user as a member of the organization"},"MemberDto":{"type":"object","properties":{"userId":{"type":"integer","description":"User identifier of the member","format":"int32"},"email":{"type":"string","description":"Member email address"},"name":{"type":"string","description":"Member display name; null if not set","nullable":true},"orgRole":{"type":"string","description":"Member role within this organization (OWNER, ADMIN, MEMBER)","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","description":"Membership status (ACTIVE, PENDING, SUSPENDED)","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]},"createdAt":{"type":"string","description":"Timestamp when the member was added to the organization","format":"date-time"}},"description":"Organization member with role and status"},"SingleValueResponseMemberDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MemberDto"}}},"CreateWebhookEndpointRequest":{"required":["subscribedEvents","url"],"type":"object","properties":{"url":{"maxLength":2048,"minLength":0,"type":"string","description":"HTTPS endpoint that receives webhook event payloads"},"description":{"maxLength":255,"minLength":0,"type":"string","description":"Optional human-readable description"},"subscribedEvents":{"minItems":1,"type":"array","description":"Event types to deliver, e.g. monitor.created, incident.resolved","items":{"minLength":1,"type":"string","description":"Event types to deliver, e.g. monitor.created, incident.resolved"}}}},"TestWebhookEndpointRequest":{"type":"object","properties":{"eventType":{"type":"string","description":"Event type to simulate (e.g. monitor.created); null uses a default","nullable":true}},"description":"Event type to use for a test webhook delivery"},"SingleValueResponseWebhookTestResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookTestResult"}}},"WebhookTestResult":{"type":"object","properties":{"success":{"type":"boolean"},"statusCode":{"type":"integer","format":"int32","nullable":true},"message":{"type":"string"},"durationMs":{"type":"integer","format":"int64","nullable":true}}},"SingleValueResponseString":{"type":"object","properties":{"data":{"type":"string","nullable":true}}},"DekRotationResultDto":{"type":"object","properties":{"previousDekVersion":{"type":"integer","description":"DEK version before rotation","format":"int32"},"newDekVersion":{"type":"integer","description":"DEK version after rotation","format":"int32"},"secretsReEncrypted":{"type":"integer","description":"Number of secrets re-encrypted with the new DEK","format":"int32"},"channelsReEncrypted":{"type":"integer","description":"Number of alert channels re-encrypted with the new DEK","format":"int32"},"rotatedAt":{"type":"string","description":"Timestamp when the rotation was performed","format":"date-time"}},"description":"Result of a data encryption key rotation operation"},"SingleValueResponseDekRotationResultDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DekRotationResultDto"}}},"CreateTagRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Tag name, unique within the org"},"color":{"pattern":"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$","type":"string","description":"Hex color code (defaults to #6B7280 if omitted)","nullable":true}},"description":"Request body for creating a tag"},"ServiceSubscribeRequest":{"type":"object","properties":{"componentId":{"type":"string","description":"ID of the component to subscribe to. Omit or null for whole-service subscription.","format":"uuid","nullable":true},"alertSensitivity":{"type":"string","description":"Alert sensitivity level. Defaults to INCIDENTS_ONLY when not provided.","nullable":true}},"description":"Optional body for subscribing to a specific component of a service"},"ComponentUptimeSummaryDto":{"type":"object","properties":{"day":{"type":"number","description":"Uptime percentage over the last 24 hours","format":"double","nullable":true,"example":99.95},"week":{"type":"number","description":"Uptime percentage over the last 7 days","format":"double","nullable":true,"example":99.98},"month":{"type":"number","description":"Uptime percentage over the last 30 days","format":"double","nullable":true,"example":99.92},"source":{"type":"string","description":"Data source: vendor_reported or incident_derived","example":"vendor_reported"}},"description":"Inline uptime percentages for 24h, 7d, 30d"},"ServiceComponentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalId":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"description":{"type":"string","nullable":true},"groupId":{"type":"string","format":"uuid","nullable":true},"position":{"type":"integer","format":"int32","nullable":true},"showcase":{"type":"boolean"},"onlyShowIfDegraded":{"type":"boolean"},"startDate":{"type":"string","format":"date-time","nullable":true},"vendorCreatedAt":{"type":"string","format":"date-time","nullable":true},"lifecycleStatus":{"type":"string"},"dataType":{"type":"string","description":"Data classification: full, status_only, or metric_only","example":"full"},"hasUptime":{"type":"boolean","description":"Whether uptime data is available for this component"},"region":{"type":"string","description":"Geographic region for regional components (AWS, GCP, Azure)","nullable":true},"groupName":{"type":"string","description":"Display name of the parent group","nullable":true},"uptime":{"$ref":"#/components/schemas/ComponentUptimeSummaryDto"},"statusChangedAt":{"type":"string","format":"date-time","nullable":true},"firstSeenAt":{"type":"string","format":"date-time"},"lastSeenAt":{"type":"string","format":"date-time"},"group":{"type":"boolean"}},"description":"A first-class service component with lifecycle and uptime data"},"ServiceSubscriptionDto":{"type":"object","properties":{"subscriptionId":{"type":"string","description":"Unique subscription identifier","format":"uuid"},"serviceId":{"type":"string","description":"Service identifier","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"logoUrl":{"type":"string","description":"Logo URL from the service catalog","nullable":true},"overallStatus":{"type":"string","description":"Current overall status; null when the service has never been polled","nullable":true},"componentId":{"type":"string","description":"Subscribed component id; null for whole-service subscription","format":"uuid","nullable":true},"component":{"$ref":"#/components/schemas/ServiceComponentDto"},"alertSensitivity":{"type":"string","description":"Alert sensitivity: ALL (synthetic + real incidents), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (real + DOWN severity)","enum":["ALL","INCIDENTS_ONLY","MAJOR_ONLY"]},"subscribedAt":{"type":"string","description":"When the organization subscribed to this service","format":"date-time"}},"description":"An org-level service subscription with current status information"},"SingleValueResponseServiceSubscriptionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceSubscriptionDto"}}},"CreateSecretRequest":{"required":["key","value"],"type":"object","properties":{"key":{"maxLength":255,"minLength":0,"type":"string","description":"Unique secret key within the workspace (max 255 chars)"},"value":{"maxLength":32768,"minLength":0,"type":"string","description":"Secret value, stored encrypted (max 32KB)"}}},"CreateResourceGroupRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this group"},"description":{"type":"string","description":"Optional description","nullable":true},"alertPolicyId":{"type":"string","description":"Optional notification policy to apply for this group","format":"uuid","nullable":true},"defaultFrequency":{"maximum":86400,"minimum":30,"type":"integer","description":"Default check frequency in seconds applied to members (30–86400)","format":"int32","nullable":true},"defaultRegions":{"type":"array","description":"Default regions applied to member monitors","nullable":true,"items":{"type":"string","description":"Default regions applied to member monitors","nullable":true}},"defaultRetryStrategy":{"$ref":"#/components/schemas/RetryStrategy"},"defaultAlertChannels":{"type":"array","description":"Default alert channel IDs applied to member monitors","nullable":true,"items":{"type":"string","description":"Default alert channel IDs applied to member monitors","format":"uuid","nullable":true}},"defaultEnvironmentId":{"type":"string","description":"Default environment ID applied to member monitors","format":"uuid","nullable":true},"healthThresholdType":{"type":"string","description":"Health threshold type: COUNT or PERCENTAGE","nullable":true,"enum":["COUNT","PERCENTAGE"]},"healthThresholdValue":{"maximum":100,"exclusiveMaximum":false,"minimum":0,"exclusiveMinimum":false,"type":"number","description":"Health threshold value: count (0+) or percentage (0–100)","nullable":true},"suppressMemberAlerts":{"type":"boolean","description":"Suppress member-level alert notifications when group manages alerting","nullable":true},"confirmationDelaySeconds":{"maximum":600,"minimum":0,"type":"integer","description":"Confirmation delay in seconds before group incident creation (0–600)","format":"int32","nullable":true},"recoveryCooldownMinutes":{"maximum":60,"minimum":0,"type":"integer","description":"Recovery cooldown in minutes after group incident resolves (0–60)","format":"int32","nullable":true}},"description":"Request body for creating a resource group"},"AddResourceGroupMemberRequest":{"required":["memberId","memberType"],"type":"object","properties":{"memberType":{"minLength":1,"pattern":"monitor|service","type":"string","description":"Type of member: 'monitor' or 'service'"},"memberId":{"type":"string","description":"ID of the monitor or service to add","format":"uuid"}},"description":"Request body for adding a member to a resource group"},"SingleValueResponseResourceGroupMemberDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupMemberDto"}}},"CreateNotificationPolicyRequest":{"required":["escalation","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this policy"},"matchRules":{"type":"array","description":"Match rules to evaluate (all must pass; omit or empty for catch-all)","items":{"$ref":"#/components/schemas/MatchRule"}},"escalation":{"$ref":"#/components/schemas/EscalationChain"},"enabled":{"type":"boolean","description":"Whether this policy is enabled (default true)","default":true},"priority":{"type":"integer","description":"Evaluation priority; higher value = evaluated first (default 0)","format":"int32","default":0}},"description":"Request body for creating a notification policy"},"TestNotificationPolicyRequest":{"type":"object","properties":{"severity":{"type":"string","description":"Incident severity to test against (e.g. DOWN, DEGRADED, MAINTENANCE)","nullable":true},"monitorId":{"type":"string","description":"Monitor UUID to test against (monitoring events)","format":"uuid","nullable":true},"regions":{"type":"array","description":"Affected region identifiers to test against (monitoring events)","nullable":true,"items":{"type":"string","description":"Affected region identifiers to test against (monitoring events)","nullable":true}},"eventType":{"type":"string","description":"Incident event type to test against — short form (e.g. created, resolved, reopened) or full form (e.g. incident.created)","nullable":true},"monitorType":{"type":"string","description":"Monitor check type to test against (e.g. HTTP, DNS, MCP_SERVER)","nullable":true},"serviceId":{"type":"string","description":"Service catalog UUID to test against (status data events)","format":"uuid","nullable":true},"componentName":{"type":"string","description":"Component name to test against (status data events, e.g. \"Actions\")","nullable":true},"resourceGroupIds":{"type":"array","description":"Resource group UUIDs the entity belongs to, for resource_group_id_in rules","nullable":true,"items":{"type":"string","description":"Resource group UUIDs the entity belongs to, for resource_group_id_in rules","format":"uuid","nullable":true}}},"description":"Event context for a dry-run match evaluation against a notification policy"},"SingleValueResponseTestMatchResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestMatchResult"}}},"TestMatchResult":{"type":"object","properties":{"matched":{"type":"boolean","description":"Whether the policy would match the supplied incident context"},"matchedRules":{"type":"array","description":"Rules that passed evaluation","items":{"type":"string","description":"Rules that passed evaluation"}},"unmatchedRules":{"type":"array","description":"Rules that did not pass evaluation","items":{"type":"string","description":"Rules that did not pass evaluation"}}},"description":"Result of a dry-run match evaluation against a notification policy"},"AlertDeliveryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"incidentId":{"type":"string","description":"Incident that triggered this delivery","format":"uuid"},"dispatchId":{"type":"string","description":"Notification dispatch that created this delivery","format":"uuid","nullable":true},"channelId":{"type":"string","description":"Alert channel ID","format":"uuid"},"channel":{"type":"string","description":"Human-readable channel name"},"channelType":{"type":"string","description":"Alert channel type (e.g. slack, email, webhook)"},"status":{"type":"string","description":"Current delivery status","enum":["PENDING","DELIVERED","RETRY_PENDING","FAILED","CANCELLED"]},"eventType":{"type":"string","description":"Incident lifecycle event that triggered this delivery","enum":["INCIDENT_CREATED","INCIDENT_RESOLVED","INCIDENT_REOPENED"]},"stepNumber":{"type":"integer","description":"1-based escalation step this delivery belongs to","format":"int32"},"fireCount":{"type":"integer","description":"Fire sequence within the step: 1 = initial, 2+ = repeat re-fires","format":"int32"},"attemptCount":{"type":"integer","description":"Number of delivery attempts made","format":"int32"},"lastAttemptAt":{"type":"string","description":"When the last attempt was made","format":"date-time","nullable":true},"nextRetryAt":{"type":"string","description":"When the next retry is scheduled (null if not retrying)","format":"date-time","nullable":true},"deliveredAt":{"type":"string","description":"Timestamp when the delivery was confirmed (null if not yet delivered)","format":"date-time","nullable":true},"errorMessage":{"type":"string","description":"Error message from the last failed attempt","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"description":"Delivery record for a single channel within a notification dispatch"},"NotificationDispatchDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique dispatch record identifier","format":"uuid"},"incidentId":{"type":"string","description":"Incident this dispatch is for","format":"uuid"},"policyId":{"type":"string","description":"Notification policy that matched this incident","format":"uuid"},"policyName":{"type":"string","description":"Human-readable name of the matched policy (null if policy has been deleted)","nullable":true},"status":{"type":"string","description":"Current dispatch state","enum":["PENDING","DISPATCHING","DELIVERED","ESCALATING","ACKNOWLEDGED","COMPLETED"]},"completionReason":{"type":"string","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.","nullable":true,"enum":["EXHAUSTED","RESOLVED","NO_STEPS"]},"currentStep":{"type":"integer","description":"1-based index of the currently active escalation step","format":"int32"},"totalSteps":{"type":"integer","description":"Total number of escalation steps in the policy (null if policy has been deleted)","format":"int32","nullable":true},"acknowledgedAt":{"type":"string","description":"Timestamp when this dispatch was acknowledged (null if not acknowledged)","format":"date-time","nullable":true},"nextEscalationAt":{"type":"string","description":"Timestamp when the next escalation step will fire (null if not scheduled)","format":"date-time","nullable":true},"lastNotifiedAt":{"type":"string","description":"Timestamp of the most recent notification delivery","format":"date-time","nullable":true},"deliveries":{"type":"array","description":"Delivery records for all channels associated with this dispatch","items":{"$ref":"#/components/schemas/AlertDeliveryDto"}},"createdAt":{"type":"string","description":"Timestamp when the dispatch was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the dispatch was last updated","format":"date-time"}},"description":"Dispatch state for a single (incident, notification policy) pair, with delivery history"},"SingleValueResponseNotificationDispatchDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/NotificationDispatchDto"}}},"CreateMonitorRequest":{"required":["config","managedBy","name","type"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this monitor"},"type":{"type":"string","description":"Monitor protocol type","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"Check frequency in seconds (30–86400, default: 60)","format":"int32"},"enabled":{"type":"boolean","description":"Whether the monitor is active (default: true)","nullable":true},"regions":{"type":"array","description":"Probe regions to run checks from, e.g. us-east, eu-west","nullable":true,"items":{"type":"string","description":"Probe regions to run checks from, e.g. us-east, eu-west","nullable":true}},"managedBy":{"type":"string","description":"Who manages this monitor: DASHBOARD or CLI","enum":["DASHBOARD","CLI"]},"environmentId":{"type":"string","description":"Environment to associate with this monitor","format":"uuid","nullable":true},"assertions":{"type":"array","description":"Assertions to evaluate against each check result","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}},"auth":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]},"incidentPolicy":{"$ref":"#/components/schemas/UpdateIncidentPolicyRequest"},"alertChannelIds":{"type":"array","description":"Alert channels to notify when this monitor triggers","nullable":true,"items":{"type":"string","description":"Alert channels to notify when this monitor triggers","format":"uuid","nullable":true}},"tags":{"$ref":"#/components/schemas/AddMonitorTagsRequest"}}},"SetMonitorAuthRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/ApiKeyAuthConfig"},{"$ref":"#/components/schemas/BasicAuthConfig"},{"$ref":"#/components/schemas/BearerAuthConfig"},{"$ref":"#/components/schemas/HeaderAuthConfig"}]}}},"AssertionTestResultDto":{"type":"object","properties":{"assertionType":{"type":"string","description":"Assertion type evaluated","enum":["status_code","response_time","body_contains","json_path","header","regex","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"]},"passed":{"type":"boolean","description":"Whether the assertion passed"},"severity":{"type":"string","description":"Assertion severity: FAIL or WARN","enum":["fail","warn"]},"message":{"type":"string","description":"Human-readable result description"},"expected":{"type":"string","description":"Expected value","nullable":true},"actual":{"type":"string","description":"Actual value observed during the test","nullable":true}}},"MonitorTestResultDto":{"type":"object","properties":{"passed":{"type":"boolean"},"error":{"type":"string","nullable":true},"statusCode":{"type":"integer","format":"int32","nullable":true},"responseTimeMs":{"type":"integer","format":"int64","nullable":true},"responseHeaders":{"type":"object","additionalProperties":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"nullable":true},"bodyPreview":{"type":"string","nullable":true},"responseSizeBytes":{"type":"integer","format":"int64","nullable":true},"redirectCount":{"type":"integer","format":"int32","nullable":true},"finalUrl":{"type":"string","nullable":true},"assertionResults":{"type":"array","items":{"$ref":"#/components/schemas/AssertionTestResultDto"}},"warnings":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}}}},"SingleValueResponseMonitorTestResultDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorTestResultDto"}}},"TableValueResultTagDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TagDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MonitorTestRequest":{"required":["config","type"],"type":"object","properties":{"type":{"type":"string","description":"Monitor protocol type to test","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"assertions":{"type":"array","description":"Optional assertions to evaluate against the test result","nullable":true,"items":{"$ref":"#/components/schemas/CreateAssertionRequest"}}}},"BulkMonitorActionRequest":{"required":["action","monitorIds"],"type":"object","properties":{"monitorIds":{"maxItems":200,"minItems":0,"type":"array","description":"IDs of monitors to act on (max 200)","items":{"type":"string","description":"IDs of monitors to act on (max 200)","format":"uuid"}},"action":{"type":"string","description":"Action to perform: PAUSE, RESUME, DELETE, ADD_TAG, REMOVE_TAG","enum":["PAUSE","RESUME","DELETE","ADD_TAG","REMOVE_TAG"]},"tagIds":{"type":"array","description":"Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)","nullable":true,"items":{"type":"string","description":"Tag IDs to attach or detach (required for ADD_TAG and REMOVE_TAG)","format":"uuid","nullable":true}},"newTags":{"type":"array","description":"New tags to create and attach (only for ADD_TAG)","nullable":true,"items":{"$ref":"#/components/schemas/NewTagRequest"}}},"description":"Request body for performing a bulk action on multiple monitors"},"BulkMonitorActionResult":{"type":"object","properties":{"succeeded":{"type":"array","description":"IDs of monitors on which the action succeeded","items":{"type":"string","description":"IDs of monitors on which the action succeeded","format":"uuid"}},"failed":{"type":"array","description":"Monitors on which the action failed, with the reason for each failure","items":{"$ref":"#/components/schemas/FailureDetail"}}},"description":"Result of a bulk monitor action, including partial-success details"},"FailureDetail":{"type":"object","properties":{"monitorId":{"type":"string","description":"Monitor ID that failed","format":"uuid"},"reason":{"type":"string","description":"Human-readable reason for the failure"}},"description":"Details about a single monitor that failed the bulk action"},"SingleValueResponseBulkMonitorActionResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BulkMonitorActionResult"}}},"CreateMaintenanceWindowRequest":{"required":["endsAt","startsAt"],"type":"object","properties":{"monitorId":{"type":"string","description":"Monitor to attach this maintenance window to; null for org-wide","format":"uuid"},"startsAt":{"type":"string","description":"Scheduled start of the maintenance window (ISO 8601)","format":"date-time"},"endsAt":{"type":"string","description":"Scheduled end of the maintenance window (ISO 8601)","format":"date-time"},"repeatRule":{"maxLength":100,"minLength":0,"type":"string","description":"iCal RRULE for recurring windows (max 100 chars); null for one-time"},"reason":{"type":"string","description":"Human-readable reason for the maintenance"},"suppressAlerts":{"type":"boolean","description":"Whether to suppress alerts during this window (default: true)"}}},"CreateInviteRequest":{"required":["email","roleOffered"],"type":"object","properties":{"email":{"minLength":1,"type":"string","description":"Email address to invite","format":"email"},"roleOffered":{"type":"string","description":"Role to assign on acceptance","enum":["OWNER","ADMIN","MEMBER"]}},"description":"Invite a new member to the organization by email"},"InviteDto":{"type":"object","properties":{"inviteId":{"type":"integer","description":"Unique invite identifier","format":"int32"},"email":{"type":"string","description":"Email address the invite was sent to"},"roleOffered":{"type":"string","description":"Role that will be assigned to the invitee on acceptance","enum":["OWNER","ADMIN","MEMBER"]},"expiresAt":{"type":"string","description":"Timestamp when the invite expires","format":"date-time"},"consumedAt":{"type":"string","description":"Timestamp when the invite was accepted; null if not yet used","format":"date-time","nullable":true},"revokedAt":{"type":"string","description":"Timestamp when the invite was revoked; null if active","format":"date-time","nullable":true}},"description":"Organization invite sent to an email address"},"SingleValueResponseInviteDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/InviteDto"}}},"CreateManualIncidentRequest":{"required":["severity","title"],"type":"object","properties":{"title":{"minLength":1,"type":"string","description":"Short summary of the incident"},"severity":{"type":"string","description":"Incident severity: DOWN, DEGRADED, or MAINTENANCE","enum":["DOWN","DEGRADED","MAINTENANCE"]},"monitorId":{"type":"string","description":"Monitor to associate with this incident","format":"uuid","nullable":true},"body":{"type":"string","description":"Detailed description or context for the incident","nullable":true}}},"IncidentDetailDto":{"type":"object","properties":{"incident":{"$ref":"#/components/schemas/IncidentDto"},"updates":{"type":"array","items":{"$ref":"#/components/schemas/IncidentUpdateDto"}}}},"IncidentUpdateDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"incidentId":{"type":"string","format":"uuid"},"oldStatus":{"type":"string","nullable":true,"enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"newStatus":{"type":"string","nullable":true,"enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"body":{"type":"string","nullable":true},"createdBy":{"type":"string","enum":["SYSTEM","USER"]},"notifySubscribers":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseIncidentDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IncidentDetailDto"}}},"AddIncidentUpdateRequest":{"type":"object","properties":{"body":{"type":"string","description":"Update message or post-mortem notes"},"newStatus":{"type":"string","description":"Updated incident status; null to keep current status","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"notifySubscribers":{"type":"boolean","description":"Whether to notify subscribers of this update"}}},"ResolveIncidentRequest":{"type":"object","properties":{"body":{"type":"string","description":"Optional resolution message or post-mortem notes"}}},"CreateEnvironmentRequest":{"required":["name","slug"],"type":"object","properties":{"name":{"maxLength":100,"minLength":0,"type":"string","description":"Human-readable environment name"},"slug":{"maxLength":100,"minLength":0,"pattern":"^[a-z0-9][a-z0-9_-]*$","type":"string","description":"URL-safe identifier (lowercase alphanumeric, hyphens, underscores)"},"variables":{"type":"object","additionalProperties":{"type":"string","description":"Initial key-value variable pairs for this environment","nullable":true},"description":"Initial key-value variable pairs for this environment","nullable":true},"isDefault":{"type":"boolean","description":"Whether this is the default environment for new monitors"}}},"AcquireDeployLockRequest":{"required":["lockedBy"],"type":"object","properties":{"lockedBy":{"minLength":1,"type":"string","description":"Identity of the lock requester (e.g. hostname, CI job ID)"},"ttlMinutes":{"type":"integer","description":"Lock TTL in minutes (default: 30, max: 60)","format":"int32","nullable":true,"example":30}},"description":"Request to acquire a deploy lock for the current workspace"},"DeployLockDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique lock identifier","format":"uuid"},"lockedBy":{"type":"string","description":"Identity of the lock holder (e.g. CLI session ID, username)"},"lockedAt":{"type":"string","description":"Timestamp when the lock was acquired","format":"date-time"},"expiresAt":{"type":"string","description":"Timestamp when the lock automatically expires","format":"date-time"}},"description":"Represents an active deploy lock for a workspace"},"SingleValueResponseDeployLockDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DeployLockDto"}}},"CreateApiKeyRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"Human-readable name to identify this API key"},"expiresAt":{"type":"string","description":"Optional expiration timestamp in ISO 8601 format","format":"date-time","nullable":true}}},"ApiKeyCreateResponse":{"type":"object","properties":{"id":{"type":"integer","description":"Unique API key identifier","format":"int32"},"name":{"type":"string","description":"Human-readable name for this API key"},"key":{"type":"string","description":"Full API key value in dh_live_* format; store this now"},"createdAt":{"type":"string","description":"Timestamp when the key was created","format":"date-time"},"expiresAt":{"type":"string","description":"Timestamp when the key expires; null if no expiration","format":"date-time","nullable":true}},"description":"Created API key with the full key value — store it now, it won't be shown again"},"SingleValueResponseApiKeyCreateResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreateResponse"}}},"ApiKeyDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique API key identifier","format":"int32"},"name":{"type":"string","description":"Human-readable name for this API key"},"key":{"type":"string","description":"Full API key value in dh_live_* format"},"createdAt":{"type":"string","description":"Timestamp when the key was created","format":"date-time"},"updatedAt":{"type":"string","description":"Timestamp when the key was last updated","format":"date-time"},"lastUsedAt":{"type":"string","description":"Timestamp of the most recent API call; null if never used","format":"date-time","nullable":true},"revokedAt":{"type":"string","description":"Timestamp when the key was revoked; null if active","format":"date-time","nullable":true},"expiresAt":{"type":"string","description":"Timestamp when the key expires; null if no expiration","format":"date-time","nullable":true}},"description":"API key for programmatic access to the DevHelm API"},"SingleValueResponseApiKeyDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyDto"}}},"SingleValueResponseAlertDeliveryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AlertDeliveryDto"}}},"CreateAlertChannelRequest":{"required":["config","name"],"type":"object","properties":{"name":{"maxLength":255,"minLength":0,"type":"string","description":"Human-readable name for this alert channel"},"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}}},"SingleValueResponseTestChannelResult":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TestChannelResult"}}},"TestChannelResult":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}},"TestAlertChannelRequest":{"required":["config"],"type":"object","properties":{"config":{"oneOf":[{"$ref":"#/components/schemas/DiscordChannelConfig"},{"$ref":"#/components/schemas/EmailChannelConfig"},{"$ref":"#/components/schemas/OpsGenieChannelConfig"},{"$ref":"#/components/schemas/PagerDutyChannelConfig"},{"$ref":"#/components/schemas/SlackChannelConfig"},{"$ref":"#/components/schemas/TeamsChannelConfig"},{"$ref":"#/components/schemas/WebhookChannelConfig"}]}},"description":"Alert channel configuration to test without saving"},"ComponentUpdateRequest":{"required":["addComponents"],"type":"object","properties":{"addComponents":{"minItems":1,"type":"array","items":{"type":"string"}}}},"UpdateAlertSensitivityRequest":{"required":["alertSensitivity"],"type":"object","properties":{"alertSensitivity":{"minLength":1,"pattern":"ALL|INCIDENTS_ONLY|MAJOR_ONLY","type":"string","description":"Alert sensitivity: ALL (any status change), INCIDENTS_ONLY (real vendor incidents, default), MAJOR_ONLY (only DOWN-level incidents)"}},"description":"Request body for updating alert sensitivity on a service subscription"},"UpdateApiKeyRequest":{"required":["name"],"type":"object","properties":{"name":{"maxLength":200,"minLength":0,"type":"string","description":"New name for this API key"}}},"TableValueResultWorkspaceDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WorkspaceDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseMapStringString":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{"type":"string","nullable":true},"nullable":true}}},"SingleValueResponseListMonitorAssertionDto":{"type":"object","properties":{"data":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/MonitorAssertionDto"}}}},"SchedulableMonitorDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique monitor identifier","format":"uuid"},"type":{"type":"string","description":"Monitor protocol type","enum":["HTTP","DNS","MCP_SERVER","TCP","ICMP","HEARTBEAT"]},"config":{"oneOf":[{"$ref":"#/components/schemas/DnsMonitorConfig"},{"$ref":"#/components/schemas/HeartbeatMonitorConfig"},{"$ref":"#/components/schemas/HttpMonitorConfig"},{"$ref":"#/components/schemas/IcmpMonitorConfig"},{"$ref":"#/components/schemas/McpServerMonitorConfig"},{"$ref":"#/components/schemas/TcpMonitorConfig"}]},"frequencySeconds":{"type":"integer","description":"Check frequency in seconds","format":"int32"},"regions":{"type":"array","description":"Probe regions to execute checks from","items":{"type":"string","description":"Probe regions to execute checks from"}},"organizationId":{"type":"integer","description":"Organization this monitor belongs to","format":"int32"}}},"TableValueResultAdapterHealthDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdapterHealthDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseListBillingPlanDto":{"type":"object","properties":{"data":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/BillingPlanDto"}}}},"TableValueResultTransactionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TransactionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultSubscriptionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseUpcomingChargeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UpcomingChargeResponse"}}},"UpcomingChargeResponse":{"type":"object","properties":{"action":{"type":"string","description":"Type of subscription action being previewed","enum":["UPGRADE","DOWNGRADE","NOOP"]},"immediateAmount":{"type":"integer","description":"Amount due immediately (proration) in smallest currency unit","format":"int32"},"nextBillingAmount":{"type":"integer","description":"Amount that will be charged on the next billing cycle","format":"int32"},"nextBillingDate":{"type":"string","description":"Date of the next billing cycle; null if cancelling","format":"date-time","nullable":true}},"description":"Preview of upcoming subscription charge after a plan change"},"EntitlementDto":{"type":"object","properties":{"key":{"type":"string","description":"Entitlement key"},"value":{"type":"integer","description":"Effective limit value (overrides applied)","format":"int64"},"defaultValue":{"type":"integer","description":"Plan-tier default value before overrides","format":"int64"},"overridden":{"type":"boolean","description":"Whether this entitlement has an org-level override"}},"description":"A single resolved entitlement for the organization"},"EntitlementResponse":{"type":"object","properties":{"tier":{"type":"string","description":"Resolved billing plan tier","enum":["FREE","STARTER","PRO","TEAM","BUSINESS","ENTERPRISE"]},"entitlements":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/EntitlementDto"},"description":"All entitlements keyed by entitlement key"},"usage":{"type":"object","additionalProperties":{"type":"integer","description":"Current usage counters keyed by entitlement key (only for countable resources)","format":"int64"},"description":"Current usage counters keyed by entitlement key (only for countable resources)"},"trialActive":{"type":"boolean","description":"Whether the org is currently on a trial"},"trialExpiresAt":{"type":"string","description":"Trial expiry date (null if not trialing)","format":"date-time","nullable":true},"subscriptionStatus":{"type":"string","description":"Current subscription status (null if no subscription)","nullable":true}},"description":"Full entitlement state for an organization: resolved limits, usage, and trial info"},"SingleValueResponseEntitlementResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EntitlementResponse"}}},"PaginationParams":{"required":["sortBy","sortOrder"],"type":"object","properties":{"sortBy":{"type":"string"},"sortOrder":{"type":"string","enum":["ASC","DESC"]},"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"maximum":200,"minimum":1,"type":"integer","format":"int32"}}},"IdValuePair":{"type":"object","properties":{"id":{"type":"integer","description":"Numeric identifier","format":"int32"},"value":{"type":"string","description":"Display label or value"}},"description":"Generic id/value pair for select options and autocomplete"},"TableValueResultIdValuePair":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IdValuePair"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MyOrgItemDto":{"type":"object","properties":{"orgId":{"type":"integer","description":"Organization identifier","format":"int32"},"orgName":{"type":"string","description":"Organization name"},"orgRole":{"type":"string","description":"Member role within this organization","enum":["OWNER","ADMIN","MEMBER"]},"status":{"type":"string","description":"Membership status","enum":["INVITED","ACTIVE","SUSPENDED","LEFT","REMOVED","DECLINED"]}},"description":"Membership summary for an organization the user belongs to"},"TableValueResultMyOrgItemDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MyOrgItemDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64","nullable":true}}},"Pageable":{"type":"object","properties":{"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"minimum":1,"type":"integer","format":"int32"},"sort":{"type":"array","items":{"type":"string"}}}},"TableValueResultUserDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UserDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"AdminStatsDto":{"type":"object","properties":{"userCount":{"type":"integer","format":"int64"},"orgCount":{"type":"integer","format":"int64"},"memberCount":{"type":"integer","format":"int64"}}},"SingleValueResponseAdminStatsDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminStatsDto"}}},"TableValueResultOrganizationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultMemberDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MemberDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultWebhookEndpointDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpointDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultWebhookDeliveryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"WebhookDeliveryDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"endpointId":{"type":"string","format":"uuid"},"eventId":{"type":"string"},"eventType":{"type":"string"},"status":{"type":"string"},"attemptCount":{"type":"integer","format":"int32"},"maxAttempts":{"type":"integer","format":"int32"},"responseStatus":{"type":"integer","format":"int32","nullable":true},"responseLatencyMs":{"type":"integer","format":"int32","nullable":true},"errorMessage":{"type":"string","nullable":true},"deliveredAt":{"type":"string","format":"date-time","nullable":true},"failedAt":{"type":"string","format":"date-time","nullable":true},"nextRetryAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"SingleValueResponseWebhookSigningSecretDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookSigningSecretDto"}}},"WebhookSigningSecretDto":{"type":"object","properties":{"configured":{"type":"boolean"},"maskedSecret":{"type":"string","nullable":true}}},"WebhookEventCatalogEntry":{"type":"object","properties":{"type":{"type":"string","description":"Dot-notation event type identifier, e.g. \"monitor.created\""},"surface":{"type":"string","description":"Product surface this event belongs to, e.g. \"monitoring\" or \"status_data\""},"description":{"type":"string","description":"Human-readable description of when this event fires"}},"description":"List of all available webhook event types"},"WebhookEventCatalogResponse":{"type":"object","properties":{"data":{"type":"array","description":"List of all available webhook event types","items":{"$ref":"#/components/schemas/WebhookEventCatalogEntry"}}}},"CursorPageServiceCatalogDto":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"$ref":"#/components/schemas/ServiceCatalogDto"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"ServiceCatalogDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"developerContext":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"overallStatus":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"componentCount":{"type":"integer","format":"int64"},"activeIncidentCount":{"type":"integer","format":"int64"},"dataCompleteness":{"type":"string"}},"description":"Items on this page"},"MaintenanceComponentRef":{"type":"object","properties":{"id":{"type":"string","description":"Component identifier","format":"uuid"},"name":{"type":"string","description":"Component name"},"status":{"type":"string","description":"Component status at the time of the maintenance update"}},"description":"A component affected by a scheduled maintenance window"},"MaintenanceUpdateDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique update identifier","format":"uuid"},"status":{"type":"string","description":"Status at the time of this update"},"body":{"type":"string","description":"Update message from the vendor","nullable":true},"displayAt":{"type":"string","description":"Timestamp when this update was posted","format":"date-time","nullable":true}},"description":"A status update within a scheduled maintenance lifecycle"},"ScheduledMaintenanceDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique maintenance record identifier","format":"uuid"},"externalId":{"type":"string","description":"Vendor-assigned maintenance identifier"},"title":{"type":"string","description":"Maintenance title as reported by the vendor"},"status":{"type":"string","description":"Current maintenance status (scheduled, in_progress, completed)"},"impact":{"type":"string","description":"Reported impact level","nullable":true},"shortlink":{"type":"string","description":"Vendor-provided short URL to the maintenance page","nullable":true},"scheduledFor":{"type":"string","description":"Timestamp when the maintenance is scheduled to begin","format":"date-time","nullable":true},"scheduledUntil":{"type":"string","description":"Timestamp when the maintenance is scheduled to end","format":"date-time","nullable":true},"startedAt":{"type":"string","description":"Timestamp when the maintenance actually started","format":"date-time","nullable":true},"completedAt":{"type":"string","description":"Timestamp when the maintenance was completed","format":"date-time","nullable":true},"affectedComponents":{"type":"array","description":"Components affected by this maintenance","items":{"$ref":"#/components/schemas/MaintenanceComponentRef"}},"updates":{"type":"array","description":"Status updates posted during the maintenance lifecycle","items":{"$ref":"#/components/schemas/MaintenanceUpdateDto"}}},"description":"A scheduled maintenance window from a vendor status page"},"ServiceDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","nullable":true},"officialStatusUrl":{"type":"string","nullable":true},"developerContext":{"type":"string","nullable":true},"logoUrl":{"type":"string","nullable":true},"adapterType":{"type":"string"},"pollingIntervalSeconds":{"type":"integer","format":"int32"},"enabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"currentStatus":{"$ref":"#/components/schemas/ServiceStatusDto"},"recentIncidents":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentDto"}},"components":{"type":"array","items":{"$ref":"#/components/schemas/ServiceComponentDto"}},"uptime":{"$ref":"#/components/schemas/ComponentUptimeSummaryDto"},"activeMaintenances":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledMaintenanceDto"}},"dataCompleteness":{"type":"string"}}},"ServiceIncidentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"serviceSlug":{"type":"string","nullable":true},"serviceName":{"type":"string","nullable":true},"externalId":{"type":"string","nullable":true},"title":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"updatedAt":{"type":"string","format":"date-time","nullable":true},"shortlink":{"type":"string","nullable":true},"detectedAt":{"type":"string","format":"date-time","nullable":true},"vendorCreatedAt":{"type":"string","format":"date-time","nullable":true}}},"ServiceStatusDto":{"type":"object","properties":{"overallStatus":{"type":"string"},"lastPolledAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseServiceDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceDetailDto"}}},"ServiceUptimeResponse":{"type":"object","properties":{"overallUptimePct":{"type":"number","description":"Overall uptime percentage across the entire period; null when no polling data exists","format":"double","nullable":true,"example":99.95},"period":{"type":"string","description":"Requested period","example":"7d"},"granularity":{"type":"string","description":"Requested granularity","example":"hourly"},"buckets":{"type":"array","description":"Per-bucket breakdown ordered by time ascending","items":{"$ref":"#/components/schemas/UptimeBucketDto"}},"source":{"type":"string","description":"Data source: vendor_reported, incident_derived, or poll_derived","nullable":true,"example":"vendor_reported"}},"description":"Uptime response with per-bucket breakdown and overall percentage for the period"},"SingleValueResponseServiceUptimeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceUptimeResponse"}}},"UptimeBucketDto":{"type":"object","properties":{"timestamp":{"type":"string","description":"Start of the bucket interval (ISO 8601)","format":"date-time","example":"2024-01-01T00:00:00Z"},"uptimePct":{"type":"number","description":"Uptime percentage for this bucket; null when no polls occurred","format":"double","nullable":true,"example":100.0},"totalPolls":{"type":"integer","description":"Total number of polls recorded in this bucket","format":"int64","example":12}},"description":"Uptime statistics for a single time bucket"},"TableValueResultScheduledMaintenanceDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ScheduledMaintenanceDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultServiceIncidentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"ServiceIncidentDetailDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"status":{"type":"string"},"impact":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"resolvedAt":{"type":"string","format":"date-time","nullable":true},"detectedAt":{"type":"string","format":"date-time","nullable":true},"shortlink":{"type":"string","nullable":true},"affectedComponents":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"updates":{"type":"array","items":{"$ref":"#/components/schemas/ServiceIncidentUpdateDto"}}}},"ServiceIncidentUpdateDto":{"type":"object","properties":{"status":{"type":"string"},"body":{"type":"string","nullable":true},"displayAt":{"type":"string","format":"date-time","nullable":true}}},"SingleValueResponseServiceIncidentDetailDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ServiceIncidentDetailDto"}}},"TableValueResultServiceComponentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceComponentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"ComponentUptimeDayDto":{"type":"object","properties":{"date":{"type":"string","description":"Date of the daily bucket (ISO 8601)","format":"date-time"},"partialOutageSeconds":{"type":"integer","description":"Seconds of partial outage observed on this day","format":"int32"},"majorOutageSeconds":{"type":"integer","description":"Seconds of major outage observed on this day","format":"int32"},"uptimePercentage":{"type":"number","description":"Computed uptime percentage for the day","format":"double"},"eventsJson":{"type":"string","description":"Incident event references for this day as raw JSON","nullable":true},"source":{"type":"string","description":"Data source: vendor_reported or incident_derived"}},"description":"Daily uptime data for a component"},"TableValueResultComponentUptimeDayDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ComponentUptimeDayDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"GlobalStatusSummaryDto":{"type":"object","properties":{"totalServices":{"type":"integer","description":"Total number of services in the catalog","format":"int32"},"operationalCount":{"type":"integer","description":"Number of services currently fully operational","format":"int32"},"degradedCount":{"type":"integer","description":"Number of services with degraded status","format":"int32"},"partialOutageCount":{"type":"integer","description":"Number of services with partial outage","format":"int32"},"majorOutageCount":{"type":"integer","description":"Number of services with major outage","format":"int32"},"maintenanceCount":{"type":"integer","description":"Number of services currently under maintenance","format":"int32"},"activeIncidentCount":{"type":"integer","description":"Total number of active incidents across all services","format":"int64"},"servicesWithIssues":{"type":"array","description":"Services that are not fully operational","items":{"$ref":"#/components/schemas/ServiceCatalogDto"}}},"description":"Global status summary across all subscribed vendor services"},"SingleValueResponseGlobalStatusSummaryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/GlobalStatusSummaryDto"}}},"TableValueResultServiceSubscriptionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceSubscriptionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultSecretDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SecretDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultResourceGroupDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ResourceGroupDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseResourceGroupHealthDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResourceGroupHealthDto"}}},"NotificationDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique notification identifier","format":"int64"},"type":{"type":"string","description":"Notification category (e.g. incident, monitor, team)"},"title":{"type":"string","description":"Short notification title"},"body":{"type":"string","description":"Full notification body; null for title-only notifications","nullable":true},"resourceType":{"type":"string","description":"Type of the resource this notification is about","nullable":true},"resourceId":{"type":"string","description":"ID of the resource this notification is about","nullable":true},"read":{"type":"boolean","description":"Whether the notification has been read"},"createdAt":{"type":"string","description":"Timestamp when the notification was created","format":"date-time"}},"description":"In-app notification for the current user"},"TableValueResultNotificationDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseLong":{"type":"object","properties":{"data":{"type":"integer","format":"int64","nullable":true}}},"TableValueResultNotificationPolicyDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationPolicyDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultNotificationDispatchDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationDispatchDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultMonitorDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MonitorDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"MonitorVersionDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique version record identifier","format":"uuid"},"monitorId":{"type":"string","description":"Monitor this version belongs to","format":"uuid"},"version":{"type":"integer","description":"Monotonically increasing version number","format":"int32"},"snapshot":{"$ref":"#/components/schemas/MonitorDto"},"changedById":{"type":"integer","description":"User ID who made the change; null for automated changes","format":"int32","nullable":true},"changedVia":{"type":"string","description":"Change source (DASHBOARD, CLI, API)","enum":["API","DASHBOARD","CLI","TERRAFORM"]},"changeSummary":{"type":"string","description":"Human-readable description of what changed","nullable":true},"createdAt":{"type":"string","description":"Timestamp when this version was recorded","format":"date-time"}},"description":"A point-in-time version snapshot of a monitor configuration"},"TableValueResultMonitorVersionDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MonitorVersionDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"SingleValueResponseMonitorVersionDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MonitorVersionDto"}}},"UptimeDto":{"type":"object","properties":{"uptimePercentage":{"type":"number","description":"Uptime percentage over the requested window; null when no data","format":"double","nullable":true,"example":99.95},"totalChecks":{"type":"integer","description":"Total number of checks executed","format":"int64","example":1440},"passedChecks":{"type":"integer","description":"Number of checks that passed","format":"int64","example":1439},"avgLatencyMs":{"type":"number","description":"Weighted average latency in milliseconds; null when no data","format":"double","nullable":true,"example":142.5},"p95LatencyMs":{"type":"number","description":"95th-percentile latency in milliseconds (upper bound across regions); null when no data","format":"double","nullable":true,"example":312.0}},"description":"Uptime statistics aggregated from continuous aggregates"},"SingleValueResponseUptimeDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UptimeDto"}}},"CursorPage":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"type":"object","description":"Items on this page"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"AssertionResultDto":{"type":"object","properties":{"type":{"type":"string","description":"Assertion type","example":"status_code"},"passed":{"type":"boolean","description":"Whether the assertion passed"},"severity":{"type":"string","description":"Assertion severity","enum":["fail","warn"]},"message":{"type":"string","description":"Human-readable result message","nullable":true},"expected":{"type":"string","description":"Expected value","nullable":true,"example":"200"},"actual":{"type":"string","description":"Actual value observed","nullable":true,"example":"503"}},"description":"Result of evaluating a single assertion against a check result"},"CheckResultDetailsDto":{"type":"object","properties":{"statusCode":{"type":"integer","description":"HTTP status code of the response","format":"int32","nullable":true,"example":200},"responseHeaders":{"type":"object","additionalProperties":{"type":"array","description":"HTTP response headers","nullable":true,"items":{"type":"string","description":"HTTP response headers","nullable":true}},"description":"HTTP response headers","nullable":true},"responseBodySnapshot":{"type":"string","description":"Raw response body snapshot (may be HTML, XML, JSON, or plain text)","nullable":true},"assertionResults":{"type":"array","description":"Individual assertion evaluation results","nullable":true,"items":{"$ref":"#/components/schemas/AssertionResultDto"}},"tlsInfo":{"$ref":"#/components/schemas/TlsInfoDto"},"redirectCount":{"type":"integer","description":"Number of HTTP redirects followed","format":"int32","nullable":true,"example":2},"redirectTarget":{"type":"string","description":"Final URL after redirects","nullable":true},"responseSizeBytes":{"type":"integer","description":"Response body size in bytes","format":"int32","nullable":true,"example":4096},"checkDetails":{"oneOf":[{"$ref":"#/components/schemas/Dns"},{"$ref":"#/components/schemas/Http"},{"$ref":"#/components/schemas/Icmp"},{"$ref":"#/components/schemas/McpServer"},{"$ref":"#/components/schemas/Tcp"}]}},"description":"Type-specific details captured during a check execution"},"CheckResultDto":{"type":"object","properties":{"id":{"type":"string","description":"Unique identifier of the check result","format":"uuid"},"timestamp":{"type":"string","description":"Timestamp when the check was executed (ISO 8601)","format":"date-time"},"region":{"type":"string","description":"Region where the check was executed","example":"us-east"},"responseTimeMs":{"type":"integer","description":"Response time in milliseconds","format":"int32","nullable":true,"example":123},"passed":{"type":"boolean","description":"Whether the check passed","example":true},"failureReason":{"type":"string","description":"Reason for failure when passed=false","nullable":true},"severityHint":{"type":"string","description":"Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing","nullable":true},"details":{"$ref":"#/components/schemas/CheckResultDetailsDto"}},"description":"A single check result from a monitor run"},"CheckTypeDetailsDto":{"required":["check_type"],"type":"object","properties":{"check_type":{"type":"string"}},"description":"Check-type-specific details — polymorphic by check_type discriminator","discriminator":{"propertyName":"check_type"}},"CursorPageCheckResultDto":{"type":"object","properties":{"data":{"type":"array","description":"Items on this page","items":{"$ref":"#/components/schemas/CheckResultDto"}},"nextCursor":{"type":"string","description":"Opaque cursor for the next page; null when there are no more results","nullable":true},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"}},"description":"Cursor-paginated response for time-series and append-only data"},"Dns":{"type":"object","description":"DNS check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"hostname":{"type":"string","description":"Target hostname","nullable":true},"requestedTypes":{"type":"array","description":"Requested DNS record types","nullable":true,"items":{"type":"string","description":"Requested DNS record types","nullable":true}},"usedResolver":{"type":"string","description":"Resolver used for lookup","nullable":true},"records":{"type":"object","additionalProperties":{"type":"array","description":"Resolved DNS records keyed by record type","nullable":true,"items":{"type":"object","additionalProperties":{"type":"object","description":"Resolved DNS records keyed by record type","nullable":true},"description":"Resolved DNS records keyed by record type","nullable":true}},"description":"Resolved DNS records keyed by record type","nullable":true},"attempts":{"type":"array","description":"DNS resolution attempts","nullable":true,"items":{"type":"object","additionalProperties":{"type":"object","description":"DNS resolution attempts","nullable":true},"description":"DNS resolution attempts","nullable":true}},"failureKind":{"type":"string","description":"Kind of DNS failure, if any","nullable":true}}}]},"Http":{"type":"object","description":"HTTP check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"timing":{"type":"object","additionalProperties":{"type":"object","description":"Request phase timing breakdown","nullable":true},"description":"Request phase timing breakdown","nullable":true},"bodyTruncated":{"type":"boolean","description":"Whether the response body was truncated before storage","nullable":true}}}]},"Icmp":{"type":"object","description":"ICMP (ping) check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"host":{"type":"string","description":"Target host","example":"1.1.1.1"},"packetsSent":{"type":"integer","description":"Number of ICMP packets sent","format":"int32","nullable":true},"packetsReceived":{"type":"integer","description":"Number of ICMP packets received","format":"int32","nullable":true},"packetLoss":{"type":"number","description":"Packet loss percentage","format":"double","nullable":true,"example":0.0},"avgRttMs":{"type":"number","description":"Average round-trip time in ms","format":"double","nullable":true},"minRttMs":{"type":"number","description":"Minimum round-trip time in ms","format":"double","nullable":true},"maxRttMs":{"type":"number","description":"Maximum round-trip time in ms","format":"double","nullable":true},"jitterMs":{"type":"number","description":"Jitter in ms","format":"double","nullable":true}}}]},"McpServer":{"type":"object","description":"MCP server check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"url":{"type":"string","description":"MCP server URL","nullable":true},"protocolVersion":{"type":"string","description":"MCP protocol version","nullable":true},"serverInfo":{"type":"object","additionalProperties":{"type":"object","description":"MCP server info (name, version, etc.)","nullable":true},"description":"MCP server info (name, version, etc.)","nullable":true},"toolCount":{"type":"integer","description":"Number of tools exposed","format":"int32","nullable":true},"resourceCount":{"type":"integer","description":"Number of resources exposed","format":"int32","nullable":true},"promptCount":{"type":"integer","description":"Number of prompts exposed","format":"int32","nullable":true}}}]},"Tcp":{"type":"object","description":"TCP check-type-specific details","allOf":[{"$ref":"#/components/schemas/CheckTypeDetailsDto"},{"type":"object","properties":{"host":{"type":"string","description":"Target host","example":"db.example.com"},"port":{"type":"integer","description":"Target port","format":"int32","example":5432},"connected":{"type":"boolean","description":"Whether a TCP connection was established"}}}]},"TlsInfoDto":{"type":"object","properties":{"subjectCn":{"type":"string","description":"Certificate subject common name","nullable":true,"example":"*.example.com"},"subjectSan":{"type":"array","description":"Subject Alternative Names","nullable":true,"items":{"type":"string","description":"Subject Alternative Names","nullable":true}},"issuerCn":{"type":"string","description":"Issuer common name","nullable":true,"example":"R3"},"issuerOrg":{"type":"string","description":"Issuer organisation","nullable":true,"example":"Let's Encrypt"},"notBefore":{"type":"string","description":"Certificate validity start (ISO 8601 UTC)","nullable":true},"notAfter":{"type":"string","description":"Certificate validity end (ISO 8601 UTC)","nullable":true},"serialNumber":{"type":"string","description":"Certificate serial number","nullable":true},"tlsVersion":{"type":"string","description":"TLS protocol version","nullable":true,"example":"TLSv1.3"},"cipherSuite":{"type":"string","description":"Negotiated cipher suite","nullable":true},"chainValid":{"type":"boolean","description":"Whether the chain validated against the OS trust store","nullable":true}},"description":"TLS/SSL certificate details for HTTPS targets"},"ChartBucketDto":{"type":"object","properties":{"bucket":{"type":"string","description":"Start of the time bucket (ISO 8601)","format":"date-time","example":"2026-03-12T10:00:00Z"},"uptimePercent":{"type":"number","description":"Uptime percentage for this bucket; null when no data","format":"double","nullable":true,"example":100.0},"avgLatencyMs":{"type":"number","description":"Weighted average latency in milliseconds for this bucket","format":"double","nullable":true,"example":120.3},"p95LatencyMs":{"type":"number","description":"95th percentile latency in milliseconds (max across regions)","format":"double","nullable":true,"example":250.0},"p99LatencyMs":{"type":"number","description":"99th percentile latency in milliseconds (max across regions)","format":"double","nullable":true,"example":480.0}},"description":"Aggregated metrics for a time bucket"},"RegionStatusDto":{"type":"object","properties":{"region":{"type":"string","description":"Region identifier","example":"us-east"},"passed":{"type":"boolean","description":"Whether the last check in this region passed","example":true},"responseTimeMs":{"type":"integer","description":"Response time in milliseconds for the last check","format":"int32","nullable":true,"example":95},"timestamp":{"type":"string","description":"Timestamp of the last check in this region (ISO 8601)","format":"date-time"},"severityHint":{"type":"string","description":"Severity hint: 'down' for hard failures, 'degraded' for warn-only failures, null when passing","nullable":true}},"description":"Latest check result for a single region"},"ResultSummaryDto":{"type":"object","properties":{"currentStatus":{"type":"string","description":"Derived current status across all regions","enum":["up","degraded","down","unknown"]},"latestPerRegion":{"type":"array","description":"Latest check result per region","items":{"$ref":"#/components/schemas/RegionStatusDto"}},"chartData":{"type":"array","description":"Time-bucketed chart data for the requested window","items":{"$ref":"#/components/schemas/ChartBucketDto"}},"uptime24h":{"type":"number","description":"Uptime percentage over the last 24 hours; null when no data","format":"double","nullable":true,"example":99.95},"uptimeWindow":{"type":"number","description":"Uptime percentage for the selected chart window; null when no data","format":"double","nullable":true,"example":99.8}},"description":"Dashboard summary: current status, per-region latest results, and chart data"},"SingleValueResponseResultSummaryDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ResultSummaryDto"}}},"TableValueResultMaintenanceWindowDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceWindowDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultInviteDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InviteDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"IntegrationCatalogResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationDto"}}}},"IntegrationConfigSchemaDto":{"type":"object","properties":{"connectionFields":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationFieldDto"}},"channelFields":{"type":"array","items":{"$ref":"#/components/schemas/IntegrationFieldDto"}}}},"IntegrationDto":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"logoUrl":{"type":"string"},"authType":{"type":"string"},"tierAvailability":{"type":"string","enum":["FREE","STARTER","PRO","TEAM","BUSINESS","ENTERPRISE"]},"lifecycle":{"type":"string"},"setupGuideUrl":{"type":"string"},"configSchema":{"$ref":"#/components/schemas/IntegrationConfigSchemaDto"}}},"IntegrationFieldDto":{"required":["key","label","required","sensitive","type"],"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"required":{"type":"boolean"},"sensitive":{"type":"boolean"},"placeholder":{"type":"string","nullable":true},"helpText":{"type":"string","nullable":true},"options":{"type":"array","nullable":true,"items":{"type":"string","nullable":true}},"default":{"type":"string","nullable":true}}},"IncidentFilterParams":{"type":"object","properties":{"status":{"type":"string","enum":["WATCHING","TRIGGERED","CONFIRMED","RESOLVED"]},"severity":{"type":"string","enum":["DOWN","DEGRADED","MAINTENANCE"]},"source":{"type":"string","enum":["AUTOMATIC","MANUAL","MONITORS","STATUS_DATA","RESOURCE_GROUP"]},"monitorId":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"resourceGroupId":{"type":"string","format":"uuid"},"tagId":{"type":"string","format":"uuid","nullable":true},"environmentId":{"type":"string","format":"uuid","nullable":true},"startedFrom":{"type":"string","format":"date-time","nullable":true},"startedTo":{"type":"string","format":"date-time","nullable":true},"page":{"minimum":0,"type":"integer","format":"int32"},"size":{"maximum":200,"minimum":1,"type":"integer","format":"int32"}}},"TableValueResultEnvironmentDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EnvironmentDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"DashboardOverviewDto":{"type":"object","properties":{"monitors":{"$ref":"#/components/schemas/MonitorsSummaryDto"},"incidents":{"$ref":"#/components/schemas/IncidentsSummaryDto"}},"description":"Combined dashboard overview for monitors and incidents"},"IncidentsSummaryDto":{"type":"object","properties":{"active":{"type":"integer","format":"int64"},"resolvedToday":{"type":"integer","format":"int64"},"mttr30d":{"type":"number","format":"double","nullable":true}},"description":"Incident summary counters"},"MonitorsSummaryDto":{"type":"object","properties":{"total":{"type":"integer","description":"Total number of monitors in the organization","format":"int64"},"up":{"type":"integer","description":"Number of monitors currently passing","format":"int64"},"down":{"type":"integer","description":"Number of monitors currently failing (DOWN severity)","format":"int64"},"degraded":{"type":"integer","description":"Number of monitors with degraded status","format":"int64"},"paused":{"type":"integer","description":"Number of disabled monitors","format":"int64"},"avgUptime24h":{"type":"number","description":"Average uptime percentage across all monitors over last 24h","format":"double","nullable":true},"avgUptime30d":{"type":"number","description":"Average uptime percentage across all monitors over last 30 days","format":"double","nullable":true}},"description":"Dashboard summary counters for monitors"},"SingleValueResponseDashboardOverviewDto":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DashboardOverviewDto"}}},"CategoryDto":{"type":"object","properties":{"category":{"type":"string","description":"Category name (e.g. CI/CD, Cloud, Payments)"},"serviceCount":{"type":"integer","description":"Number of services in this category","format":"int64"}},"description":"Service category with its count of catalog entries"},"TableValueResultCategoryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CategoryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"AuthMeResponse":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/KeyInfo"},"organization":{"$ref":"#/components/schemas/OrgInfo"},"plan":{"$ref":"#/components/schemas/PlanInfo"},"rateLimits":{"$ref":"#/components/schemas/RateLimitInfo"}},"description":"Identity, organization, plan, and rate-limit info for the authenticated API key"},"KeyInfo":{"type":"object","properties":{"id":{"type":"integer","description":"Key ID","format":"int32"},"name":{"type":"string","description":"Human-readable key name"},"createdAt":{"type":"string","description":"When the key was created","format":"date-time"},"expiresAt":{"type":"string","description":"When the key expires (null = never)","format":"date-time","nullable":true},"lastUsedAt":{"type":"string","description":"Last time the key was used","format":"date-time","nullable":true}},"description":"API key metadata"},"OrgInfo":{"type":"object","properties":{"id":{"type":"integer","description":"Organization ID","format":"int32"},"name":{"type":"string","description":"Organization name"}},"description":"Organization the key belongs to"},"PlanInfo":{"type":"object","properties":{"tier":{"type":"string","description":"Resolved plan tier","enum":["FREE","STARTER","PRO","TEAM","BUSINESS","ENTERPRISE"]},"subscriptionStatus":{"type":"string","description":"Subscription status (null if no subscription)","nullable":true},"trialActive":{"type":"boolean","description":"Whether the org is on a trial"},"trialExpiresAt":{"type":"string","description":"Trial expiry (null if not trialing)","format":"date-time","nullable":true},"entitlements":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/EntitlementDto"},"description":"Entitlement limits keyed by entitlement name"},"usage":{"type":"object","additionalProperties":{"type":"integer","description":"Current usage counters keyed by entitlement name","format":"int64"},"description":"Current usage counters keyed by entitlement name"}},"description":"Billing plan and entitlement state"},"RateLimitInfo":{"type":"object","properties":{"requestsPerMinute":{"type":"integer","description":"Maximum requests allowed per window","format":"int64"},"remaining":{"type":"integer","description":"Requests remaining in the current window","format":"int64"},"windowMs":{"type":"integer","description":"Sliding window size in milliseconds","format":"int64"}},"description":"Rate-limit quota for the current sliding window"},"SingleValueResponseAuthMeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AuthMeResponse"}}},"AuditEventDto":{"type":"object","properties":{"id":{"type":"integer","description":"Unique audit event identifier","format":"int64"},"actorId":{"type":"integer","description":"User ID who performed the action; null for system actions","format":"int32","nullable":true},"actorEmail":{"type":"string","description":"Email of the actor; null for system actions","nullable":true},"action":{"type":"string","description":"Audit action type (e.g. monitor.created, api_key.revoked)"},"resourceType":{"type":"string","description":"Type of resource affected (e.g. monitor, api_key)","nullable":true},"resourceId":{"type":"string","description":"ID of the affected resource","nullable":true},"resourceName":{"type":"string","description":"Human-readable name of the affected resource","nullable":true},"metadata":{"type":"object","additionalProperties":{"type":"object","description":"Additional context about the action","nullable":true},"description":"Additional context about the action","nullable":true},"createdAt":{"type":"string","description":"Timestamp when the action was performed","format":"date-time"}}},"PageResultAuditEventDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditEventDto"}},"page":{"type":"integer","format":"int32"},"size":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"totalPages":{"type":"integer","format":"int32"},"hasNext":{"type":"boolean"}}},"TableValueResultApiKeyDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeyDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"DeliveryAttemptDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"deliveryId":{"type":"string","format":"uuid"},"attemptNumber":{"type":"integer","description":"1-based attempt number","format":"int32"},"status":{"type":"string","description":"Outcome: SUCCESS, FAILED, TIMEOUT, ERROR"},"responseStatusCode":{"type":"integer","description":"HTTP response status code from the external service","format":"int32","nullable":true},"requestPayload":{"type":"string","description":"JSON payload sent to the external service","nullable":true},"responseBody":{"type":"string","description":"Response body from the external service (truncated)","nullable":true},"errorMessage":{"type":"string","description":"Error message if the attempt failed","nullable":true},"responseTimeMs":{"type":"integer","description":"Round-trip time in milliseconds","format":"int32","nullable":true},"externalId":{"type":"string","description":"External identifier (e.g. PagerDuty dedup_key, SES MessageId, webhook delivery UUID)","nullable":true},"requestHeaders":{"type":"object","additionalProperties":{"type":"string","description":"HTTP request headers sent to the external service","nullable":true},"description":"HTTP request headers sent to the external service","nullable":true},"attemptedAt":{"type":"string","format":"date-time"}},"description":"Single delivery attempt with request/response audit data"},"TableValueResultDeliveryAttemptDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryAttemptDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultAlertChannelDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AlertChannelDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"TableValueResultAlertDeliveryDto":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AlertDeliveryDto"}},"hasNext":{"type":"boolean"},"hasPrev":{"type":"boolean"}}},"RemoveMonitorTagsRequest":{"required":["tagIds"],"type":"object","properties":{"tagIds":{"minItems":1,"type":"array","description":"IDs of the tags to detach from the monitor","items":{"type":"string","description":"IDs of the tags to detach from the monitor","format":"uuid"}}},"description":"Request body for removing tags from a monitor"},"DeleteChannelResult":{"type":"object","properties":{"affectedPolicies":{"type":"integer","description":"Number of notification policies whose escalation steps were modified","format":"int32"},"disabledPolicies":{"type":"integer","description":"Number of notification policies disabled because they had no remaining channels","format":"int32"}},"description":"Summary of policies affected by channel deletion"}},"securitySchemes":{"BearerAuth":{"type":"http","description":"API key (dh_live_...) or Auth0 JWT token","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file diff --git a/src/commands/monitors/versions/index.ts b/src/commands/monitors/versions/list.ts similarity index 88% rename from src/commands/monitors/versions/index.ts rename to src/commands/monitors/versions/list.ts index 1631bfa..afe4bad 100644 --- a/src/commands/monitors/versions/index.ts +++ b/src/commands/monitors/versions/list.ts @@ -8,9 +8,9 @@ type MonitorVersionDto = components['schemas']['MonitorVersionDto'] export default class MonitorsVersionsList extends Command { static description = 'List version history for a monitor' static examples = [ - '<%= config.bin %> monitors versions 42', - '<%= config.bin %> monitors versions 42 --limit 5', - '<%= config.bin %> monitors versions 42 -o json', + '<%= config.bin %> monitors versions list 42', + '<%= config.bin %> monitors versions list 42 --limit 5', + '<%= config.bin %> monitors versions list 42 -o json', ] static args = {id: Args.string({description: 'Monitor ID', required: true})} diff --git a/src/lib/api.generated.ts b/src/lib/api.generated.ts index 7743e99..bca7c5b 100644 --- a/src/lib/api.generated.ts +++ b/src/lib/api.generated.ts @@ -200,7 +200,8 @@ export interface paths { path?: never; cookie?: never; }; - get?: never; + /** Get a tag by ID */ + get: operations["getById"]; /** Update a tag's name and/or color */ put: operations["update_2"]; post?: never; @@ -311,7 +312,7 @@ export interface paths { cookie?: never; }; /** Get a notification policy by ID */ - get: operations["getById"]; + get: operations["getById_1"]; /** Update a notification policy */ put: operations["update_6"]; post?: never; @@ -461,7 +462,7 @@ export interface paths { cookie?: never; }; /** Get a single maintenance window by ID */ - get: operations["getById_1"]; + get: operations["getById_2"]; /** Update a maintenance window */ put: operations["update_11"]; post?: never; @@ -2347,7 +2348,7 @@ export interface paths { * 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_2"]; + get: operations["getById_3"]; put?: never; post?: never; delete?: never; @@ -3384,7 +3385,7 @@ export interface components { SingleValueResponseResourceGroupDto: { data?: components["schemas"]["ResourceGroupDto"]; }; - /** @description Escalation chain defining which channels to notify */ + /** @description Escalation chain defining which channels to notify; null preserves current */ EscalationChain: { /** @description Ordered escalation steps, evaluated in sequence */ steps: components["schemas"]["EscalationStep"][]; @@ -3423,20 +3424,20 @@ export interface components { /** @description Values list for multi-value rules like monitor_type_in */ values?: (string | null)[] | null; }; - /** @description Request body for updating a notification policy */ + /** @description Request body for updating a notification policy (null fields are preserved) */ UpdateNotificationPolicyRequest: { - /** @description Human-readable name for this policy */ - name: string; + /** @description Human-readable name for this policy; null preserves current */ + 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 */ - enabled: boolean; + escalation?: components["schemas"]["EscalationChain"]; + /** @description Whether this policy is enabled; null preserves current */ + enabled?: boolean; /** * Format: int32 - * @description Evaluation priority; higher value = evaluated first + * @description Evaluation priority; higher value = evaluated first; null preserves current */ - priority: number; + priority?: number; }; /** @description Org-level notification policy with match rules and escalation chain */ NotificationPolicyDto: { @@ -7632,6 +7633,28 @@ export interface operations { }; }; }; + getById: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["SingleValueResponseTagDto"]; + }; + }; + }; + }; update_2: { parameters: { query?: never; @@ -7876,7 +7899,7 @@ export interface operations { }; }; }; - getById: { + getById_1: { parameters: { query?: never; header?: never; @@ -8283,7 +8306,7 @@ export interface operations { }; }; }; - getById_1: { + getById_2: { parameters: { query?: never; header?: never; @@ -11426,7 +11449,7 @@ export interface operations { }; }; }; - getById_2: { + getById_3: { parameters: { query?: never; header?: never; diff --git a/src/lib/descriptions.generated.ts b/src/lib/descriptions.generated.ts index 7c50d82..8a702f1 100644 --- a/src/lib/descriptions.generated.ts +++ b/src/lib/descriptions.generated.ts @@ -45,10 +45,10 @@ export const fieldDescriptions: Record> = "priority": "Evaluation priority; higher value = evaluated first (default 0)" }, "UpdateNotificationPolicyRequest": { - "name": "Human-readable name for this policy", + "name": "Human-readable name for this policy; null preserves current", "matchRules": "Match rules to evaluate (all must pass; omit or empty for catch-all)", - "enabled": "Whether this policy is enabled", - "priority": "Evaluation priority; higher value = evaluated first" + "enabled": "Whether this policy is enabled; null preserves current", + "priority": "Evaluation priority; higher value = evaluated first; null preserves current" }, "CreateEnvironmentRequest": { "name": "Human-readable environment name",