/**
* Shared Datadog transport: site resolution, API + application key secrets,
* JSON requests, and dry-run mutation previews.
*
* Auth is secret-backed (not OAuth). Default user secrets are `datadogApiKey`
* and `datadogApplicationKey`. Extra accounts use `account: "work"` →
* `datadogApiKey-work` / `datadogApplicationKey-work`. Placeholders resolve
* on approved hosts only.
*/
export const DEFAULT_API_KEY_SECRET = 'datadogApiKey'
export const DEFAULT_APP_KEY_SECRET = 'datadogApplicationKey'
export const DEFAULT_SITE = 'datadoghq.com'
export const DEFAULT_API_HOST = 'api.datadoghq.com'
export const DATADOG_API_HOSTS = [
'api.datadoghq.com',
'api.us3.datadoghq.com',
'api.us5.datadoghq.com',
'api.datadoghq.eu',
'api.ap1.datadoghq.com',
'api.ap2.datadoghq.com',
'api.uk1.datadoghq.com',
'api.ddog-gov.com',
'api.us2.ddog-gov.com',
] as const
export const ALLOWED_HOSTS_QUERY = DATADOG_API_HOSTS.join(',')
const SITE_ALIASES: Record<string, string> = {
us1: 'datadoghq.com',
us: 'datadoghq.com',
us3: 'us3.datadoghq.com',
us5: 'us5.datadoghq.com',
eu: 'datadoghq.eu',
eu1: 'datadoghq.eu',
ap1: 'ap1.datadoghq.com',
ap2: 'ap2.datadoghq.com',
uk1: 'uk1.datadoghq.com',
gov: 'ddog-gov.com',
'us1-fed': 'ddog-gov.com',
us1fed: 'ddog-gov.com',
'us2-fed': 'us2.ddog-gov.com',
us2fed: 'us2.ddog-gov.com',
}
const SECRET_SUFFIX = '(?:-[A-Za-z0-9][A-Za-z0-9_-]{0,47})?'
const API_KEY_SECRET_PATTERN = new RegExp('^datadogApiKey' + SECRET_SUFFIX + '$')
const APP_KEY_SECRET_PATTERN = new RegExp('^datadogApplicationKey' + SECRET_SUFFIX + '$')
const ACCOUNT_LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/
export type DatadogAuthOptions = {
/**
* Extra Datadog account label. `work` reads `datadogApiKey-work` and
* `datadogApplicationKey-work`. Omit (or pass `default`) for the defaults.
*/
account?: string
/** Override the API key secret name. Must be `datadogApiKey` or `datadogApiKey-<label>`. */
apiKeySecretName?: string
/** Override the application key secret name. Must be `datadogApplicationKey` or `datadogApplicationKey-<label>`. */
applicationKeySecretName?: string
/**
* Datadog site alias (`us1`, `eu`, `us3`, `us5`, `ap1`, `ap2`, `uk1`, `gov`)
* or site domain (`datadoghq.eu`). Defaults to US1 `datadoghq.com`.
*/
site?: string
}
export type DatadogJson = Record<string, any>
export class DatadogApiError extends Error {
status: number
label: string
details: unknown
apiKeySetupUrl: string
applicationKeySetupUrl: string
constructor(
message: string,
input: {
status: number
label?: string
details?: unknown
apiKeySetupUrl?: string
applicationKeySetupUrl?: string
},
) {
super(message)
this.name = 'DatadogApiError'
this.status = input.status
this.label = input.label ?? 'Datadog API'
this.details = input.details ?? null
this.apiKeySetupUrl = input.apiKeySetupUrl ?? API_KEY_SETUP_URL
this.applicationKeySetupUrl = input.applicationKeySetupUrl ?? APPLICATION_KEY_SETUP_URL
}
}
export function assertNever(value: never, message: string): never {
throw new Error(message + String(value))
}
export function parseAction<T extends string>(
value: unknown,
allowed: readonly T[],
fallback: T,
label: string,
): T {
const action = (value == null || value === '' ? fallback : value) as unknown
if (typeof action === 'string' && (allowed as readonly string[]).includes(action)) {
return action as T
}
throw new Error(
'Unknown ' + label + ' action: ' + String(action) + '. Valid actions: ' + allowed.join(', '),
)
}
export function clean(value: unknown): string {
return String(value ?? '').trim()
}
export function positiveInt(value: unknown, fallback: number, max = 100): number {
const number = Math.floor(Number(value))
if (!Number.isFinite(number) || number <= 0) return fallback
return Math.min(number, max)
}
function accountLabel(value: string | undefined): string | null {
const trimmed = (value ?? '').trim()
if (!trimmed || trimmed === 'default') return null
if (!ACCOUNT_LABEL_PATTERN.test(trimmed)) {
throw new Error(
'account must be a short label such as "work" or "live" (letters, numbers, _ or -).',
)
}
return trimmed
}
export function resolveApiKeySecretName(input: DatadogAuthOptions = {}): string {
if (input.apiKeySecretName != null && String(input.apiKeySecretName).trim() !== '') {
const name = String(input.apiKeySecretName).trim()
if (!API_KEY_SECRET_PATTERN.test(name)) {
throw new Error(
'apiKeySecretName must be datadogApiKey or datadogApiKey-<account>. Got ' +
name +
'. Save it at ' +
secretSetupUrl(name) +
'.',
)
}
return name
}
const account = accountLabel(input.account)
return account ? DEFAULT_API_KEY_SECRET + '-' + account : DEFAULT_API_KEY_SECRET
}
export function resolveApplicationKeySecretName(input: DatadogAuthOptions = {}): string {
if (input.applicationKeySecretName != null && String(input.applicationKeySecretName).trim() !== '') {
const name = String(input.applicationKeySecretName).trim()
if (!APP_KEY_SECRET_PATTERN.test(name)) {
throw new Error(
'applicationKeySecretName must be datadogApplicationKey or datadogApplicationKey-<account>. Got ' +
name +
'. Save it at ' +
secretSetupUrl(name) +
'.',
)
}
return name
}
const account = accountLabel(input.account)
return account ? DEFAULT_APP_KEY_SECRET + '-' + account : DEFAULT_APP_KEY_SECRET
}
export function secretSetupUrl(secretName: string): string {
const description =
secretName.startsWith(DEFAULT_APP_KEY_SECRET)
? 'Datadog application key for read and management API calls'
: 'Datadog API key for the HTTP API'
return (
'https://kody.codes/account/secrets/new?name=' +
encodeURIComponent(secretName) +
'&description=' +
encodeURIComponent(description) +
'&allowedHosts=' +
ALLOWED_HOSTS_QUERY +
'&scope=user'
)
}
export const API_KEY_SETUP_URL = secretSetupUrl(DEFAULT_API_KEY_SECRET)
export const APPLICATION_KEY_SETUP_URL = secretSetupUrl(DEFAULT_APP_KEY_SECRET)
function secretPlaceholder(name: string): string {
return '{{secret:' + name + '|scope=user}}'
}
export function resolveSite(input: DatadogAuthOptions = {}): string {
const raw = clean(input.site).toLowerCase()
if (!raw) return DEFAULT_SITE
if (SITE_ALIASES[raw]) return SITE_ALIASES[raw]
const stripped = raw.replace(/^https?:\/\//, '').replace(/\/+$/, '')
if (stripped.startsWith('api.')) {
const site = stripped.slice('api.'.length)
if (DATADOG_API_HOSTS.includes(('api.' + site) as (typeof DATADOG_API_HOSTS)[number])) {
return site
}
}
if (DATADOG_API_HOSTS.includes(('api.' + stripped) as (typeof DATADOG_API_HOSTS)[number])) {
return stripped
}
throw new Error(
'site must be a Datadog site alias (us1, eu, us3, us5, ap1, ap2, uk1, gov) or domain. Got ' +
raw +
'.',
)
}
export function resolveApiHost(input: DatadogAuthOptions = {}): string {
return 'api.' + resolveSite(input)
}
export function resolveApiBaseUrl(input: DatadogAuthOptions = {}): string {
return 'https://' + resolveApiHost(input)
}
export function datadogAuthHeaders(input: DatadogAuthOptions = {}): Record<string, string> {
return {
'DD-API-KEY': secretPlaceholder(resolveApiKeySecretName(input)),
'DD-APPLICATION-KEY': secretPlaceholder(resolveApplicationKeySecretName(input)),
}
}
export async function parseJsonResponse(
response: Response,
label = 'Datadog API',
input: DatadogAuthOptions = {},
): Promise<any> {
const text = await response.text()
let body: any = null
try {
body = text ? JSON.parse(text) : null
} catch {
body = { raw: text }
}
if (!response.ok) {
const apiKey = resolveApiKeySecretName(input)
const appKey = resolveApplicationKeySecretName(input)
const hint =
response.status === 401 || response.status === 403
? ' Save valid keys at ' +
secretSetupUrl(apiKey) +
' and ' +
secretSetupUrl(appKey) +
'.'
: ''
const errors = Array.isArray(body?.errors) ? body.errors.join('; ') : ''
throw new DatadogApiError(
label + ' HTTP ' + response.status + (errors ? ': ' + errors : '.') + hint,
{
status: response.status,
label,
details: body,
apiKeySetupUrl: secretSetupUrl(apiKey),
applicationKeySetupUrl: secretSetupUrl(appKey),
},
)
}
return body
}
export async function datadogRequest(
path: string,
input: DatadogAuthOptions & {
method?: string
query?: Record<string, unknown>
body?: unknown
label?: string
} = {},
): Promise<any> {
const suffix = path.startsWith('/') ? path : '/' + path
const url = new URL(resolveApiBaseUrl(input) + suffix)
for (const [key, value] of Object.entries(input.query ?? {})) {
if (value === undefined || value === null) continue
if (Array.isArray(value)) {
for (const item of value) {
if (item !== undefined && item !== null && clean(item)) {
url.searchParams.append(key, String(item))
}
}
continue
}
if (clean(value) || typeof value === 'number' || typeof value === 'boolean') {
url.searchParams.set(key, String(value))
}
}
const method = (input.method ?? 'GET').toUpperCase()
const headers: Record<string, string> = {
...datadogAuthHeaders(input),
Accept: 'application/json',
}
let body: string | undefined
if (input.body !== undefined && method !== 'GET' && method !== 'HEAD') {
body = JSON.stringify(input.body)
headers['content-type'] = 'application/json'
}
const response = await fetch(url, { method, headers, body })
return await parseJsonResponse(
response,
input.label ?? 'Datadog ' + method + ' ' + path,
input,
)
}
export function requireConfirm(input: { confirm?: boolean }, action: string) {
if (input.confirm !== true) {
throw new Error(
'Refusing to ' +
action +
' without confirm: true. This action changes live Datadog state; pass dryRun: true to preview, or confirm: true to proceed.',
)
}
}
export type DatadogDryRun = {
dryRun: true
action: string
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE'
path: string
host: string
query?: Record<string, unknown>
body?: unknown
}
export type MutationGuardInput = DatadogAuthOptions & {
dryRun?: boolean
confirm?: boolean
}
/**
* Preview a mutation when `dryRun: true`. Live mutations also require
* `confirm: true` before they contact Datadog.
*/
export function mutationPreview(
input: MutationGuardInput,
options: {
action: string
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE'
path: string
query?: Record<string, unknown>
body?: unknown
},
): DatadogDryRun | null {
if (input.dryRun === true) {
return {
dryRun: true,
action: options.action,
method: options.method,
path: options.path,
host: resolveApiHost(input),
query: options.query,
body: options.body,
}
}
requireConfirm(input, options.action)
return null
}
export function asStringArray(value: unknown): string[] {
if (Array.isArray(value)) return value.map((item) => clean(item)).filter(Boolean)
const text = clean(value)
if (!text) return []
return text
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}