export const API_BASE_URL = 'https://api.netlify.com/api/v1'
export const API_KEY_SECRET = 'netlifyToken'
export const API_HOSTS = ['api.netlify.com', 'app.netlify.com']
export const SECRET_URL =
'https://kody.codes/account/secrets/new?name=netlifyToken&description=Netlify%20personal%20access%20token%20(Bearer%20for%20api.netlify.com)&allowedHosts=api.netlify.com&scope=user'
export const TOKEN_DASHBOARD_URL = 'https://app.netlify.com/user/applications#personal-access-tokens'
export const API_DOCS_URL = 'https://docs.netlify.com/api-and-cli-guides/api-guides/get-started-with-api/'
const RETIRED_ALIASES = new Set([
'kent',
'kentcdodds',
'epicweb',
'epic-web',
'kcd',
'kentcdodds.com',
])
export function setupUrls() {
return {
tokenUrl: SECRET_URL,
tokenDashboardUrl: TOKEN_DASHBOARD_URL,
apiDocsUrl: API_DOCS_URL,
hosts: [...API_HOSTS],
tokenSecret: API_KEY_SECRET,
}
}
export function assertNoRetiredAlias(value, fieldName) {
if (typeof value !== 'string') return
const normalized = value.trim().toLowerCase()
if (!RETIRED_ALIASES.has(normalized)) return
throw new Error(
`Hard-coded Netlify account alias "${value}" (${fieldName}) is not supported. Pass secretName for a personal access token. Save a token at ${SECRET_URL}.`,
)
}
export function authSetupMessage(prefix = 'Netlify credentials are missing or were rejected.') {
return `${prefix} Save a personal access token at ${SECRET_URL} (approve host api.netlify.com). Create the token at ${TOKEN_DASHBOARD_URL}.`
}
export function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
export function withoutKeys(record, keys) {
const blocked = new Set(keys)
return Object.fromEntries(
Object.entries(record ?? {})
.filter(([, value]) => value !== undefined)
.filter(([key]) => !blocked.has(key)),
)
}
export const AUTH_AND_CONTROL_KEYS = [
'action',
'body',
'headers',
'params',
'dryRun',
'confirm',
'secretName',
'account',
'path',
'method',
]
export function queryFromInput(input, ignoredKeys = []) {
if (isRecord(input?.query)) return { ...input.query }
return withoutKeys(input, [...AUTH_AND_CONTROL_KEYS, ...ignoredKeys])
}
export function bodyFromInput(input, ignoredKeys = []) {
if (isRecord(input?.body)) return input.body
return withoutKeys(input, [...AUTH_AND_CONTROL_KEYS, 'query', ...ignoredKeys])
}
export function authFromInput(input) {
return {
secretName: input?.secretName,
account: input?.account,
}
}
export function requireConfirm(input, action, method, path, body) {
if (input?.dryRun) {
return { dryRun: true, method, path, body: body ?? null, query: input?.query ?? null }
}
if (input?.confirm !== true) {
throw new Error(
`${action} writes live Netlify state. Pass dryRun: true to preview, or confirm: true to apply.`,
)
}
return null
}
export function redactEnvValue(body) {
if (Array.isArray(body)) return body.map(redactEnvValue)
if (!isRecord(body)) return body
const next = { ...body }
if (Object.prototype.hasOwnProperty.call(next, 'value')) next.value = '[redacted]'
if (Array.isArray(next.values)) {
next.values = next.values.map((entry) => {
if (!isRecord(entry)) return entry
if (!Object.prototype.hasOwnProperty.call(entry, 'value')) return { ...entry }
return { ...entry, value: '[redacted]' }
})
}
if (Array.isArray(next.environment)) {
next.environment = next.environment.map(redactEnvValue)
}
return next
}
export function slimUser(user) {
if (!isRecord(user)) return user
return {
id: user.id,
email: user.email ?? null,
full_name: user.full_name ?? user.fullName ?? null,
avatar_url: user.avatar ?? user.avatar_url ?? null,
slug: user.slug ?? null,
}
}
export function slimAccount(account) {
if (!isRecord(account)) return account
return {
id: account.id,
name: account.name ?? null,
slug: account.slug ?? null,
type: account.type_name ?? account.type ?? null,
role: account.role ?? null,
}
}
export function slimDeploy(deploy) {
if (!isRecord(deploy)) return deploy
return {
id: deploy.id,
site_id: deploy.site_id ?? null,
state: deploy.state ?? null,
name: deploy.name ?? null,
url: deploy.url ?? null,
ssl_url: deploy.ssl_url ?? null,
deploy_url: deploy.deploy_ssl_url ?? deploy.deploy_url ?? null,
admin_url: deploy.admin_url ?? null,
branch: deploy.branch ?? null,
context: deploy.context ?? null,
draft: deploy.draft === true,
error_message: deploy.error_message ?? null,
commit_ref: deploy.commit_ref ?? null,
title: deploy.title ?? null,
created_at: deploy.created_at ?? null,
published_at: deploy.published_at ?? null,
framework: deploy.framework ?? null,
}
}
export function slimSite(site) {
if (!isRecord(site)) return site
const published = isRecord(site.published_deploy) ? slimDeploy(site.published_deploy) : null
return {
id: site.id,
name: site.name ?? null,
url: site.url ?? null,
ssl_url: site.ssl_url ?? null,
admin_url: site.admin_url ?? null,
state: site.state ?? null,
custom_domain: site.custom_domain ?? null,
account_id: site.account_id ?? null,
account_name: site.account_name ?? null,
account_slug: site.account_slug ?? null,
created_at: site.created_at ?? null,
updated_at: site.updated_at ?? null,
published_deploy: published,
}
}
export function slimEnvVar(env) {
if (!isRecord(env)) return env
const values = Array.isArray(env.values)
? env.values.map((entry) => {
if (!isRecord(entry)) return { context: null }
return {
id: entry.id ?? null,
context: entry.context ?? null,
context_parameter: entry.context_parameter ?? null,
}
})
: []
return {
key: env.key,
scopes: env.scopes ?? null,
is_secret: env.is_secret === true,
updated_at: env.updated_at ?? null,
values,
}
}
export function slimForm(form) {
if (!isRecord(form)) return form
return {
id: form.id,
site_id: form.site_id ?? null,
name: form.name ?? null,
paths: form.paths ?? null,
submission_count: form.submission_count ?? null,
created_at: form.created_at ?? null,
}
}
export function slimSubmission(submission) {
if (!isRecord(submission)) return submission
return {
id: submission.id,
number: submission.number ?? null,
email: submission.email ?? null,
name: submission.name ?? null,
summary: submission.summary ?? null,
created_at: submission.created_at ?? null,
site_url: submission.site_url ?? null,
data: isRecord(submission.data) ? submission.data : null,
}
}
export function paginationFromInput(input = {}) {
const page = input.page ?? input.query?.page
const perPage = input.per_page ?? input.perPage ?? input.query?.per_page
return {
page: page == null ? null : Number(page),
per_page: perPage == null ? null : Number(perPage),
}
}