export const API_BASE_URL = 'https://api.vercel.com'
export const API_KEY_SECRET = 'vercelToken'
export const DEFAULT_INTEGRATION_NAME = 'vercel'
export const API_HOSTS = ['api.vercel.com', 'vercel.com']
export const SECRET_URL =
'https://kody.codes/account/secrets/new?name=vercelToken&description=Vercel%20access%20token%20(Bearer%20for%20api.vercel.com)&allowedHosts=api.vercel.com&scope=user'
export const OAUTH_URL =
'https://kody.codes/connect/oauth?provider=vercel&authorizeUrl=https%3A%2F%2Fvercel.com%2Foauth%2Fauthorize&tokenUrl=https%3A%2F%2Fapi.vercel.com%2Flogin%2Foauth%2Ftoken&flow=pkce&pkce=true&scopes=openid%20email%20profile%20offline_access&allowedHosts=api.vercel.com%2Cvercel.com&apiBaseUrl=https%3A%2F%2Fapi.vercel.com&dashboardUrl=https%3A%2F%2Fvercel.com%2Fdocs%2Fsign-in-with-vercel&providerSetupInstructions=Create%20a%20Sign%20in%20with%20Vercel%20app%20and%20register%20redirect%20URI%20exactly%20https%3A%2F%2Fkody.codes%2Fconnect%2Foauth.%20REST%20API%20permissions%20for%20this%20OAuth%20lane%20are%20in%20private%20beta%20%E2%80%94%20prefer%20an%20access%20token%20for%20teams%2C%20projects%2C%20and%20deployments.%20Paste%20the%20client%20id%20into%20this%20form.'
export const TOKEN_DASHBOARD_URL = 'https://vercel.com/account/tokens'
export const OAUTH_DASHBOARD_URL = 'https://vercel.com/docs/sign-in-with-vercel'
export const REDIRECT_URI = 'https://kody.codes/connect/oauth'
const RETIRED_ALIASES = new Set(['kent', 'kentcdodds', 'epicweb', 'epic-web', 'kcd'])
export function setupUrls() {
return {
tokenUrl: SECRET_URL,
oauthUrl: OAUTH_URL,
tokenDashboardUrl: TOKEN_DASHBOARD_URL,
oauthDashboardUrl: OAUTH_DASHBOARD_URL,
redirectUri: REDIRECT_URI,
hosts: [...API_HOSTS],
tokenSecret: API_KEY_SECRET,
integrationName: DEFAULT_INTEGRATION_NAME,
}
}
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 Vercel account alias "${value}" (${fieldName}) is not supported. Pass secretName for an access token or integrationName for OAuth. Save a token at ${SECRET_URL} or connect OAuth at ${OAUTH_URL}.`,
)
}
export function authSetupMessage(prefix = 'Vercel credentials are missing or were rejected.') {
return `${prefix} Save an access token at ${SECRET_URL} (approve host api.vercel.com) or connect OAuth at ${OAUTH_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',
'integrationName',
'account',
'teamId',
'teamSlug',
'slug',
]
export function queryFromInput(input, ignoredKeys = []) {
if (isRecord(input?.query)) return { ...teamQuery(input), ...input.query }
return { ...teamQuery(input), ...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,
integrationName: input?.integrationName,
account: input?.account,
}
}
export function teamQuery(input = {}) {
const query = {}
if (typeof input.teamId === 'string' && input.teamId.trim()) query.teamId = input.teamId.trim()
const slug = typeof input.teamSlug === 'string' ? input.teamSlug : input.slug
if (typeof slug === 'string' && slug.trim() && !query.teamId) query.slug = slug.trim()
return query
}
export function requireConfirm(input, action, method, path, body) {
if (input?.dryRun) {
return { dryRun: true, method, path, body: body ?? null, query: teamQuery(input) }
}
if (input?.confirm !== true) {
throw new Error(
`${action} writes live Vercel state. Pass dryRun: true to preview, or confirm: true to apply.`,
)
}
return null
}
export function redactEnvValue(body) {
if (!isRecord(body)) return body
if (!Object.prototype.hasOwnProperty.call(body, 'value')) return body
return { ...body, value: '[redacted]' }
}
export function slimUser(user) {
if (!isRecord(user)) return user
return {
id: user.id,
username: user.username,
name: user.name ?? null,
email: user.email ?? null,
defaultTeamId: user.defaultTeamId ?? null,
}
}
export function slimTeam(team) {
if (!isRecord(team)) return team
return {
id: team.id,
name: team.name,
slug: team.slug,
createdAt: team.createdAt ?? null,
limited: team.limited === true,
}
}
export function slimProject(project) {
if (!isRecord(project)) return project
const link = isRecord(project.link)
? {
type: project.link.type ?? null,
repo: project.link.repo ?? project.link.repoId ?? null,
org: project.link.org ?? null,
}
: null
return {
id: project.id,
name: project.name,
framework: project.framework ?? null,
updatedAt: project.updatedAt ?? null,
link,
}
}
export function slimDeployment(deployment) {
if (!isRecord(deployment)) return deployment
return {
id: deployment.uid ?? deployment.id,
name: deployment.name ?? null,
url: deployment.url ?? null,
state: deployment.readyState ?? deployment.state ?? null,
target: deployment.target ?? null,
createdAt: deployment.createdAt ?? null,
inspectorUrl: deployment.inspectorUrl ?? null,
projectId: deployment.projectId ?? null,
}
}
export function slimDomain(domain) {
if (!isRecord(domain)) return domain
return {
name: domain.name ?? domain.apexName ?? null,
verified: domain.verified ?? null,
gitBranch: domain.gitBranch ?? null,
redirect: domain.redirect ?? null,
projectId: domain.projectId ?? null,
}
}
export function slimEnvVar(env) {
if (!isRecord(env)) return env
return {
id: env.id ?? null,
key: env.key,
type: env.type ?? null,
target: env.target ?? null,
gitBranch: env.gitBranch ?? null,
createdAt: env.createdAt ?? null,
updatedAt: env.updatedAt ?? null,
comment: env.comment ?? null,
}
}
export function paginationFrom(body) {
if (isRecord(body?.pagination)) {
return {
count: body.pagination.count ?? null,
next: body.pagination.next ?? null,
prev: body.pagination.prev ?? null,
}
}
return null
}