/**
* Shared Resend transport: secret-backed API keys or a saved OAuth
* integration, list unwrapping, dry-run previews, and setup-aware errors.
*
* Default names are `resend` (secret or integration). Extra accounts use
* `resend-<label>` such as `resend-work`. There are no hard-coded aliases.
*/
export const API_BASE_URL = 'https://api.resend.com'
export const API_HOST = 'api.resend.com'
export const DASHBOARD_URL = 'https://resend.com'
export const DASHBOARD_API_KEYS_URL = 'https://resend.com/api-keys'
export const OAUTH_AUTHORIZE_URL = 'https://api.resend.com/oauth/authorize'
export const OAUTH_TOKEN_URL = 'https://api.resend.com/oauth/token'
export const DEFAULT_AUTH_NAME = 'resend'
export const OAUTH_SCOPES = {
sendOnly: 'emails:send',
fullAccess: 'full_access',
} as const
const AUTH_NAME_PATTERN = /^resend(?:-[A-Za-z0-9][A-Za-z0-9_-]{0,47})?$/
const ACCOUNT_LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/
export type ResendAuthOptions = {
/**
* Extra account label. `work` reads secret/integration `resend-work`.
* Omit (or pass `default`) for `resend`.
*/
account?: string
/** Override the API key secret name. Must be `resend` or `resend-<label>`. */
secretName?: string
/**
* Saved OAuth integration name (`resend` or `resend-<label>`). When set,
* calls use `createAuthenticatedFetch` instead of the API key secret.
*/
integration?: string
}
export type ResendObject = Record<string, any>
export class ResendApiError extends Error {
status: number | null
body: unknown
method: string | null
path: string | null
setupUrl: string | null
constructor(
message: string,
meta: {
status?: number | null
body?: unknown
method?: string | null
path?: string | null
setupUrl?: string | null
} = {},
) {
super(message)
this.name = 'ResendApiError'
this.status = meta.status ?? null
this.body = meta.body ?? null
this.method = meta.method ?? null
this.path = meta.path ?? null
this.setupUrl = meta.setupUrl ?? null
}
}
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(', '),
)
}
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
}
function assertAuthName(name: string, kind: 'secretName' | 'integration'): string {
if (!AUTH_NAME_PATTERN.test(name)) {
throw new Error(
kind +
' must be resend or resend-<account>. Got ' +
name +
'. Save it at ' +
(kind === 'integration' ? oauthConnectUrl(name) : secretSetupUrl(name)) +
'.',
)
}
return name
}
function resolveBaseName(input: ResendAuthOptions = {}): string {
const account = accountLabel(input.account)
return account ? DEFAULT_AUTH_NAME + '-' + account : DEFAULT_AUTH_NAME
}
/** Resolve the user-scoped Resend API key secret name for this call. */
export function resolveSecretName(input: ResendAuthOptions = {}): string {
if (input.secretName != null && String(input.secretName).trim() !== '') {
return assertAuthName(String(input.secretName).trim(), 'secretName')
}
return resolveBaseName(input)
}
/** Resolve a saved OAuth integration name, or undefined for the API-key lane. */
export function resolveIntegrationName(input: ResendAuthOptions = {}): string | undefined {
if (input.integration == null || String(input.integration).trim() === '') return undefined
return assertAuthName(String(input.integration).trim().toLowerCase(), 'integration')
}
export function secretSetupUrl(secretName: string = DEFAULT_AUTH_NAME): string {
const params = new URLSearchParams({
name: secretName,
description:
'Resend API key (re_) for transactional email, domains, audiences, and templates',
allowedHosts: API_HOST,
scope: 'user',
})
return 'https://kody.codes/account/secrets/new?' + params.toString()
}
export function oauthConnectUrl(
provider: string = DEFAULT_AUTH_NAME,
scope: string = OAUTH_SCOPES.fullAccess,
): string {
const params = new URLSearchParams({
provider,
authorizeUrl: OAUTH_AUTHORIZE_URL,
tokenUrl: OAUTH_TOKEN_URL,
scopes: scope,
allowedHosts: API_HOST,
apiBaseUrl: API_BASE_URL,
dashboardUrl: DASHBOARD_URL,
})
return 'https://kody.codes/connect/oauth?' + params.toString()
}
export function nextSetupStep(
auth: ResendAuthOptions,
kind: 'missing-token' | 'missing-scope' | 'unverified-domain',
scope?: string,
): string {
const integration = resolveIntegrationName(auth)
if (integration) {
if (kind === 'missing-scope' && scope) {
return (
'Reconnect the ' +
integration +
' OAuth integration with the ' +
scope +
' scope: ' +
oauthConnectUrl(integration, scope)
)
}
if (kind === 'unverified-domain') {
return (
'The from address must use a verified sending domain. Call listDomains(), add the DNS records, then verifyDomain. Dashboard: ' +
DASHBOARD_URL +
'/domains'
)
}
return 'Reconnect the ' + integration + ' OAuth integration: ' + oauthConnectUrl(integration)
}
const secretName = resolveSecretName(auth)
if (kind === 'unverified-domain') {
return (
'The from address must use a verified sending domain. Call listDomains(), add the DNS records, then verifyDomain. Dashboard: ' +
DASHBOARD_URL +
'/domains'
)
}
if (kind === 'missing-scope' && scope) {
return (
'Create a Resend API key with access that covers ' +
scope +
', then update secret ' +
secretName +
' at ' +
secretSetupUrl(secretName)
)
}
return (
'Save a Resend API key as secret ' +
secretName +
' at ' +
secretSetupUrl(secretName) +
' (create one at ' +
DASHBOARD_API_KEYS_URL +
').'
)
}
export function unwrapList(body: unknown): ResendObject[] {
if (Array.isArray(body)) return body
const record = (body ?? {}) as { data?: unknown }
return Array.isArray(record.data) ? record.data : []
}
export function emailSummary(email: ResendObject | null | undefined) {
if (!email?.id) return null
return {
id: email.id,
from: email.from ?? null,
to: email.to ?? null,
subject: email.subject ?? null,
last_event: email.last_event ?? null,
created_at: email.created_at ?? null,
scheduled_at: email.scheduled_at ?? null,
}
}
export function domainSummary(domain: ResendObject | null | undefined) {
if (!domain?.id) return null
return {
id: domain.id,
name: domain.name ?? null,
status: domain.status ?? null,
region: domain.region ?? null,
created_at: domain.created_at ?? null,
}
}
export function contactSummary(contact: ResendObject | null | undefined) {
if (!contact?.id) return null
return {
id: contact.id,
email: contact.email ?? null,
first_name: contact.first_name ?? null,
last_name: contact.last_name ?? null,
unsubscribed: contact.unsubscribed ?? null,
created_at: contact.created_at ?? null,
}
}
export function broadcastSummary(broadcast: ResendObject | null | undefined) {
if (!broadcast?.id) return null
return {
id: broadcast.id,
name: broadcast.name ?? null,
subject: broadcast.subject ?? null,
status: broadcast.status ?? null,
created_at: broadcast.created_at ?? null,
scheduled_at: broadcast.scheduled_at ?? null,
editUrl: DASHBOARD_URL + '/broadcasts/' + broadcast.id,
}
}
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 Resend state; pass dryRun: true to preview, or confirm: true to proceed.',
)
}
}
export type ResendDryRun = {
dryRun: true
action: string
method: 'POST' | 'PATCH' | 'DELETE'
path: string
body?: unknown
}
export type MutationGuardInput = ResendAuthOptions & {
dryRun?: boolean
confirm?: boolean
}
/**
* Preview a mutation when `dryRun: true`. Send/delete helpers also require
* `confirm: true` before they contact Resend.
*/
export function mutationPreview(
input: MutationGuardInput,
options: {
action: string
method: 'POST' | 'PATCH' | 'DELETE'
path: string
body?: unknown
requireConfirm?: boolean
},
): ResendDryRun | null {
if (input.dryRun === true) {
return {
dryRun: true,
action: options.action,
method: options.method,
path: options.path,
body: options.body,
}
}
if (options.requireConfirm) requireConfirm(input, options.action)
return null
}
function hasHeader(headers: Record<string, string>, name: string) {
const lower = name.toLowerCase()
return Object.keys(headers).some((key) => key.toLowerCase() === lower)
}
function detailMessage(body: unknown): string | null {
if (!body || typeof body !== 'object') {
return typeof body === 'string' && body.length > 0 ? body : null
}
const record = body as { message?: unknown; name?: unknown }
if (typeof record.message === 'string' && record.message) {
return (typeof record.name === 'string' && record.name ? record.name + ': ' : '') + record.message
}
return null
}
function toResendApiError(
response: Response,
parsed: unknown,
auth: ResendAuthOptions,
meta: { method: string; path: string },
): ResendApiError {
const detail = detailMessage(parsed)
const setupUrl = resolveIntegrationName(auth)
? oauthConnectUrl(resolveIntegrationName(auth))
: secretSetupUrl(resolveSecretName(auth))
const lower = (detail ?? '').toLowerCase()
if (response.status === 401) {
return new ResendApiError(
'Resend authentication failed (HTTP 401). The API key or OAuth token is missing, invalid, or expired. ' +
nextSetupStep(auth, 'missing-token'),
{ status: 401, body: parsed, method: meta.method, path: meta.path, setupUrl },
)
}
if (response.status === 403) {
const domainIssue =
lower.includes('domain') || lower.includes('from') || lower.includes('not verified')
const scopeIssue = lower.includes('scope') || lower.includes('permission') || lower.includes('unauthorized')
const kind = domainIssue ? 'unverified-domain' : scopeIssue ? 'missing-scope' : 'missing-token'
const scope = lower.includes('full_access')
? OAUTH_SCOPES.fullAccess
: lower.includes('emails:send')
? OAUTH_SCOPES.sendOnly
: OAUTH_SCOPES.fullAccess
return new ResendApiError(
(detail ? detail + ' ' : 'Resend request was forbidden (HTTP 403). ') +
nextSetupStep(auth, kind, scope),
{ status: 403, body: parsed, method: meta.method, path: meta.path, setupUrl },
)
}
return new ResendApiError(
detail ??
'Resend API ' +
response.status +
(meta.method ? ' ' + meta.method : '') +
(meta.path ? ' ' + meta.path : ''),
{ status: response.status, body: parsed, method: meta.method, path: meta.path, setupUrl },
)
}
const integrationFetchCache = new Map<string, typeof fetch>()
async function getIntegrationFetch(name: string): Promise<typeof fetch> {
const cached = integrationFetchCache.get(name)
if (cached) return cached
const { createAuthenticatedFetch } = await import('kody:runtime')
const authed = await createAuthenticatedFetch(name)
integrationFetchCache.set(name, authed)
return authed
}
export type ResendRequestInput = ResendAuthOptions & {
path: string
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'
query?: Record<string, string | number | boolean | null | undefined>
body?: unknown
headers?: Record<string, string>
idempotencyKey?: string
fetchImpl?: typeof fetch
}
function buildUrl(path: string, query: ResendRequestInput['query'] = {}) {
const normalized = path.startsWith('/') ? path : '/' + path
const url = new URL(API_BASE_URL + normalized)
if (url.origin !== API_BASE_URL) {
throw new Error('Resend requests must stay on https://api.resend.com.')
}
for (const [key, value] of Object.entries(query ?? {})) {
if (value === undefined || value === null) continue
url.searchParams.append(key, String(value))
}
return url
}
/** Authenticated Resend request. Reads always execute; helpers gate writes. */
export async function resendRequest(input: ResendRequestInput): Promise<any> {
const method = input.method ?? 'GET'
const url = buildUrl(input.path, input.query)
const integration = resolveIntegrationName(input)
const headers: Record<string, string> = { Accept: 'application/json', ...(input.headers ?? {}) }
if (input.body !== undefined && !hasHeader(headers, 'content-type')) {
headers['content-type'] = 'application/json'
}
if (input.idempotencyKey) headers['Idempotency-Key'] = input.idempotencyKey
let fetchImpl = input.fetchImpl ?? fetch
if (!input.fetchImpl && integration) {
fetchImpl = await getIntegrationFetch(integration)
} else if (!integration) {
headers.Authorization = 'Bearer {{secret:' + resolveSecretName(input) + '|scope=user}}'
}
const response = await fetchImpl(url, {
method,
headers,
body: input.body === undefined ? undefined : JSON.stringify(input.body),
})
const text = await response.text()
let parsed: unknown = null
try {
parsed = text ? JSON.parse(text) : null
} catch {
parsed = text
}
if (!response.ok) {
throw toResendApiError(response, parsed, input, { method, path: input.path })
}
return parsed
}
export type GenericRequestInput = ResendRequestInput & {
/** Writes default to dry-run. Pass `dryRun: false` to execute. */
dryRun?: boolean
}
/**
* Escape-hatch REST call. GET/HEAD/OPTIONS always execute. Other methods
* default to `{ dryRun: true, wouldCall }` until `dryRun: false`.
*/
export async function resendCall(input: GenericRequestInput) {
const method = (input.method ?? 'GET').toUpperCase()
const isRead = method === 'GET' || method === 'HEAD' || method === 'OPTIONS'
if (!isRead && input.dryRun !== false) {
return {
dryRun: true,
wouldCall: {
method,
path: input.path,
query: input.query,
body: input.body,
},
}
}
return resendRequest(input)
}