hvac-marketing-skills/tools/clis/resend.js
Corey Haines ebdf1dd2f1 fix: correct API issues found by second codex review across 14 CLIs
Fixes from thorough codex review (o3 high reasoning, 5 parallel batches):

Validation fixes:
- customer-io: add ID validation for customer/campaign commands, event name check
- dub: fix links get to use /links/info endpoint, add --id validation
- google-search-console: fix countries to use ['country'] only, add --url validation
- mention-me: add --customer-id validation on referral/share/reward commands
- tolt: add --id validation for affiliates get/update

Auth & API fixes:
- apollo: move API key from header to JSON body, fix search endpoint path
- rewardful: change from Bearer to Basic auth
- hotjar: split OAuth URL (unversioned) from resource URL (v2)
- amplitude: wrap retention e param in JSON array
- snov: change list prospects from GET to POST with JSON body
- optimizely: change archive from DELETE to PATCH status=archived
- google-ads: fix budget body field from camelCase to snake_case
- resend: change webhook field from endpoint to url, add validation
- linkedin-ads: add required campaignGroup URN, fix numeric amount types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:31:29 -08:00

356 lines
12 KiB
JavaScript
Executable file

#!/usr/bin/env node
const API_KEY = process.env.RESEND_API_KEY
const BASE_URL = 'https://api.resend.com'
if (!API_KEY) {
console.error(JSON.stringify({ error: 'RESEND_API_KEY environment variable required' }))
process.exit(1)
}
async function api(method, path, body) {
if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined }
}
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
const text = await res.text()
try {
return JSON.parse(text)
} catch {
return { status: res.status, body: text }
}
}
function parseArgs(args) {
const result = { _: [] }
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const key = arg.slice(2)
const next = args[i + 1]
if (next && !next.startsWith('--')) {
result[key] = next
i++
} else {
result[key] = true
}
} else {
result._.push(arg)
}
}
return result
}
const args = parseArgs(process.argv.slice(2))
const [cmd, sub, ...rest] = args._
async function main() {
let result
switch (cmd) {
case 'send': {
const body = { from: args.from, to: args.to?.split(','), subject: args.subject }
if (args.html) body.html = args.html
if (args.text) body.text = args.text
if (args.cc) body.cc = args.cc.split(',')
if (args.bcc) body.bcc = args.bcc.split(',')
if (args['reply-to']) body.reply_to = args['reply-to']
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
if (args.tags) body.tags = args.tags.split(',').map(t => {
const [name, value] = t.split(':')
return { name, value }
})
result = await api('POST', '/emails', body)
break
}
case 'emails':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/emails?${params}`)
break
}
case 'get':
result = await api('GET', `/emails/${rest[0]}`)
break
case 'cancel':
result = await api('POST', `/emails/${rest[0]}/cancel`)
break
default:
result = { error: 'Unknown emails subcommand. Use: list, get, cancel' }
}
break
case 'domains':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/domains?${params}`)
break
}
case 'get':
result = await api('GET', `/domains/${rest[0]}`)
break
case 'create':
result = await api('POST', '/domains', { name: args.name, region: args.region })
break
case 'verify':
result = await api('POST', `/domains/${rest[0]}/verify`)
break
case 'delete':
result = await api('DELETE', `/domains/${rest[0]}`)
break
default:
result = { error: 'Unknown domains subcommand. Use: list, get, create, verify, delete' }
}
break
case 'api-keys':
switch (sub) {
case 'list':
result = await api('GET', '/api-keys')
break
case 'create': {
const body = { name: args.name }
if (args.permission) body.permission = args.permission
if (args.domain_id) body.domain_id = args.domain_id
result = await api('POST', '/api-keys', body)
break
}
case 'delete':
result = await api('DELETE', `/api-keys/${rest[0]}`)
break
default:
result = { error: 'Unknown api-keys subcommand. Use: list, create, delete' }
}
break
case 'audiences':
switch (sub) {
case 'list':
result = await api('GET', '/audiences')
break
case 'get':
result = await api('GET', `/audiences/${rest[0]}`)
break
case 'create':
result = await api('POST', '/audiences', { name: args.name })
break
case 'delete':
result = await api('DELETE', `/audiences/${rest[0]}`)
break
default:
result = { error: 'Unknown audiences subcommand. Use: list, get, create, delete' }
}
break
case 'contacts': {
const audienceId = sub
const action = rest[0]
const contactId = rest[1]
switch (action) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/audiences/${audienceId}/contacts?${params}`)
break
}
case 'get':
result = await api('GET', `/audiences/${audienceId}/contacts/${contactId}`)
break
case 'create': {
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args.unsubscribed) body.unsubscribed = args.unsubscribed === 'true'
result = await api('POST', `/audiences/${audienceId}/contacts`, body)
break
}
case 'update': {
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
if (args.unsubscribed !== undefined) body.unsubscribed = args.unsubscribed === 'true'
result = await api('PATCH', `/audiences/${audienceId}/contacts/${contactId}`, body)
break
}
case 'delete':
result = await api('DELETE', `/audiences/${audienceId}/contacts/${contactId}`)
break
default:
result = { error: 'Unknown contacts action. Use: list, get, create, update, delete' }
}
break
}
case 'webhooks':
switch (sub) {
case 'list':
result = await api('GET', '/webhooks')
break
case 'get':
result = await api('GET', `/webhooks/${rest[0]}`)
break
case 'create': {
if (!args.url) { result = { error: '--url required (webhook URL)' }; break }
const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced']
result = await api('POST', '/webhooks', { url: args.url, events })
break
}
case 'delete':
result = await api('DELETE', `/webhooks/${rest[0]}`)
break
default:
result = { error: 'Unknown webhooks subcommand. Use: list, get, create, delete' }
}
break
case 'batch': {
const emails = JSON.parse(args.emails || '[]')
result = await api('POST', '/emails/batch', emails)
break
}
case 'templates':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
if (args.after) params.set('after', args.after)
if (args.before) params.set('before', args.before)
result = await api('GET', `/templates?${params}`)
break
}
case 'get':
result = await api('GET', `/templates/${rest[0]}`)
break
case 'create': {
const body = { name: args.name, html: args.html }
if (args.alias) body.alias = args.alias
if (args.from) body.from = args.from
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
result = await api('POST', '/templates', body)
break
}
case 'update': {
const body = {}
if (args.name) body.name = args.name
if (args.html) body.html = args.html
if (args.alias) body.alias = args.alias
if (args.from) body.from = args.from
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
result = await api('PATCH', `/templates/${rest[0]}`, body)
break
}
case 'delete':
result = await api('DELETE', `/templates/${rest[0]}`)
break
case 'publish':
result = await api('POST', `/templates/${rest[0]}/publish`)
break
case 'duplicate':
result = await api('POST', `/templates/${rest[0]}/duplicate`)
break
default:
result = { error: 'Unknown templates subcommand. Use: list, get, create, update, delete, publish, duplicate' }
}
break
case 'broadcasts':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/broadcasts?${params}`)
break
}
case 'get':
result = await api('GET', `/broadcasts/${rest[0]}`)
break
case 'create': {
const body = { segment_id: args['segment-id'], from: args.from, subject: args.subject }
if (args.html) body.html = args.html
if (args.text) body.text = args.text
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.name) body.name = args.name
result = await api('POST', '/broadcasts', body)
break
}
case 'send': {
const body = {}
if (args['scheduled-at']) body.scheduled_at = args['scheduled-at']
result = await api('POST', `/broadcasts/${rest[0]}/send`, body)
break
}
case 'delete':
result = await api('DELETE', `/broadcasts/${rest[0]}`)
break
default:
result = { error: 'Unknown broadcasts subcommand. Use: list, get, create, send, delete' }
}
break
case 'segments':
switch (sub) {
case 'list': {
const params = new URLSearchParams()
if (args.limit) params.set('limit', args.limit)
result = await api('GET', `/segments?${params}`)
break
}
case 'get':
result = await api('GET', `/segments/${rest[0]}`)
break
case 'create':
result = await api('POST', '/segments', { name: args.name })
break
case 'delete':
result = await api('DELETE', `/segments/${rest[0]}`)
break
default:
result = { error: 'Unknown segments subcommand. Use: list, get, create, delete' }
}
break
default:
result = {
error: 'Unknown command',
usage: {
send: 'send --from <email> --to <email> --subject <subject> --html <html>',
emails: 'emails [list|get|cancel] [id]',
domains: 'domains [list|get|create|verify|delete] [id] [--name <name>]',
'api-keys': 'api-keys [list|create|delete] [id] [--name <name>]',
audiences: 'audiences [list|get|create|delete] [id] [--name <name>]',
contacts: 'contacts <audience_id> [list|get|create|update|delete] [contact_id] [--email <email>]',
webhooks: 'webhooks [list|get|create|delete] [id] [--endpoint <url>]',
batch: 'batch --emails <json_array>',
templates: 'templates [list|get|create|update|delete|publish|duplicate] [id] [--name <name>] [--html <html>] [--variables <json>]',
broadcasts: 'broadcasts [list|get|create|send|delete] [id] [--segment-id <id>] [--from <email>] [--subject <subject>]',
segments: 'segments [list|get|create|delete] [id] [--name <name>]',
}
}
}
console.log(JSON.stringify(result, null, 2))
}
main().catch(err => {
console.error(JSON.stringify({ error: err.message }))
process.exit(1)
})