/** * Authenticated Vercel REST client. * Auth: access-token secret `vercelToken`, or a saved OAuth integration. * Host approval is enforced by Kody's fetch gateway. */ import { createAuthenticatedFetch } from 'kody:runtime' import { API_BASE_URL, API_KEY_SECRET, assertNoRetiredAlias, authSetupMessage, } from './setup.js' export function buildUrl(pathTemplate, params = {}) { return API_BASE_URL + pathTemplate.replace(/\{([^}]+)\}/g, (_match, name) => { const value = params[name] if (value === undefined || value === null || value === '') { throw new Error(`Missing required path parameter: ${name}`) } return encodeURIComponent(String(value)) }) } export function appendQuery(url, query = {}) { const search = new URLSearchParams() for (const [key, value] of Object.entries(query ?? {})) { if (value === undefined || value === null || value === '') continue if (Array.isArray(value)) { for (const item of value) { if (item === undefined || item === null || item === '') continue search.append(key, String(item)) } continue } search.append(key, String(value)) } const qs = search.toString() return qs ? `${url}?${qs}` : url } function mergeHeaders(userHeaders, authHeaders) { const merged = { ...(userHeaders ?? {}) } for (const [key, value] of Object.entries(authHeaders)) { const lower = key.toLowerCase() for (const existing of Object.keys(merged)) { if (existing.toLowerCase() === lower) delete merged[existing] } merged[key] = value } return merged } function hasHeader(headers, name) { const lower = name.toLowerCase() return Object.keys(headers).some((key) => key.toLowerCase() === lower) } function assertSecretName(name) { if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)) { throw new Error( 'Vercel secretName must be a safe secret identifier (letters, numbers, dot, underscore, hyphen).', ) } return name } function resolveSecretName(input = {}) { assertNoRetiredAlias(input.secretName, 'secretName') assertNoRetiredAlias(input.account, 'account') if (typeof input.secretName === 'string' && input.secretName.trim()) { return assertSecretName(input.secretName.trim()) } if (typeof input.account === 'string' && input.account.trim()) { return assertSecretName(`${API_KEY_SECRET}-${input.account.trim()}`) } return API_KEY_SECRET } async function resolveTransport(input = {}) { assertNoRetiredAlias(input.integrationName, 'integrationName') if (typeof input.integrationName === 'string' && input.integrationName.trim()) { try { return { fetchImpl: await createAuthenticatedFetch(input.integrationName.trim()), authHeaders: {}, } } catch (error) { const message = error instanceof Error ? error.message : String(error) throw new Error( authSetupMessage( `Vercel OAuth integration "${input.integrationName}" is not connected. ${message}`, ), ) } } return { fetchImpl: fetch, authHeaders: { Authorization: `Bearer {{secret:${resolveSecretName(input)}}}` }, } } /** * Escape-hatch fetch for Vercel REST paths. * `path` is relative to https://api.vercel.com (include `/v9/projects`, etc.). */ export async function rawVercelRequest(path, options = {}) { if (typeof path !== 'string' || path.trim() === '') { throw new Error('Vercel request requires a non-empty path string.') } if (/^https?:\/\//i.test(path)) { throw new Error('Vercel request path must be relative, for example /v2/user.') } const normalized = path.startsWith('/') ? path : `/${path}` const url = appendQuery(`${API_BASE_URL}${normalized}`, options.query) const { fetchImpl, authHeaders } = await resolveTransport(options) const headers = mergeHeaders( { Accept: 'application/json', ...(options.headers ?? {}), }, authHeaders, ) let body = options.body if (body !== undefined && typeof body !== 'string') { body = JSON.stringify(body) if (!hasHeader(headers, 'content-type')) headers['content-type'] = 'application/json' } return fetchImpl(url, { method: options.method ?? 'GET', headers, body }) }