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>
This commit is contained in:
Corey Haines 2026-02-17 14:31:29 -08:00
parent aad399682c
commit ebdf1dd2f1
14 changed files with 62 additions and 29 deletions

View file

@ -149,7 +149,7 @@ async function main() {
params.set('start', args.start) params.set('start', args.start)
params.set('end', args.end) params.set('end', args.end)
if (args.event) { if (args.event) {
params.set('e', JSON.stringify({ event_type: args.event })) params.set('e', JSON.stringify([{ event_type: args.event }]))
} }
result = await queryApi('GET', '/retention', params) result = await queryApi('GET', '/retention', params)
break break

View file

@ -9,17 +9,16 @@ if (!API_KEY) {
} }
async function api(method, path, body) { async function api(method, path, body) {
const authBody = body ? { ...body, api_key: API_KEY } : { api_key: API_KEY }
if (args['dry-run']) { if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'x-api-key': '***', 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: body || undefined } return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { 'Content-Type': 'application/json' }, body: { ...authBody, api_key: '***' } }
} }
const res = await fetch(`${BASE_URL}${path}`, { const res = await fetch(`${BASE_URL}${path}`, {
method, method,
headers: { headers: {
'x-api-key': API_KEY,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json',
}, },
body: body ? JSON.stringify(body) : undefined, body: JSON.stringify(authBody),
}) })
const text = await res.text() const text = await res.text()
try { try {
@ -67,7 +66,7 @@ async function main() {
if (args.seniorities) body.person_seniorities = args.seniorities.split(',') if (args.seniorities) body.person_seniorities = args.seniorities.split(',')
if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim()) if (args['employee-ranges']) body.organization_num_employees_ranges = args['employee-ranges'].split(',').map(r => r.trim())
if (args.keywords) body.q_keywords = args.keywords if (args.keywords) body.q_keywords = args.keywords
result = await api('POST', '/mixed_people/api_search', body) result = await api('POST', '/mixed_people/search', body)
break break
} }
case 'enrich': { case 'enrich': {

View file

@ -94,6 +94,7 @@ async function main() {
switch (sub) { switch (sub) {
case 'identify': { case 'identify': {
const customerId = rest[0] || args.id const customerId = rest[0] || args.id
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
const body = {} const body = {}
if (args.email) body.email = args.email if (args.email) body.email = args.email
if (args['first-name']) body.first_name = args['first-name'] if (args['first-name']) body.first_name = args['first-name']
@ -106,16 +107,20 @@ async function main() {
} }
case 'get': { case 'get': {
const customerId = rest[0] || args.id const customerId = rest[0] || args.id
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
result = await appApi('GET', `/customers/${customerId}/attributes`) result = await appApi('GET', `/customers/${customerId}/attributes`)
break break
} }
case 'delete': { case 'delete': {
const customerId = rest[0] || args.id const customerId = rest[0] || args.id
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
result = await trackApi('DELETE', `/customers/${customerId}`) result = await trackApi('DELETE', `/customers/${customerId}`)
break break
} }
case 'track-event': { case 'track-event': {
const customerId = rest[0] || args.id const customerId = rest[0] || args.id
if (!customerId) { result = { error: 'Customer ID required (positional arg or --id)' }; break }
if (!args.name) { result = { error: '--name required (event name)' }; break }
const body = { name: args.name } const body = { name: args.name }
if (args.data) body.data = JSON.parse(args.data) if (args.data) body.data = JSON.parse(args.data)
result = await trackApi('POST', `/customers/${customerId}/events`, body) result = await trackApi('POST', `/customers/${customerId}/events`, body)
@ -131,18 +136,26 @@ async function main() {
case 'list': case 'list':
result = await appApi('GET', '/campaigns') result = await appApi('GET', '/campaigns')
break break
case 'get': case 'get': {
result = await appApi('GET', `/campaigns/${rest[0]}`) const campaignId = rest[0] || args.id
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
result = await appApi('GET', `/campaigns/${campaignId}`)
break break
case 'metrics': }
result = await appApi('GET', `/campaigns/${rest[0]}/metrics`) case 'metrics': {
const campaignId = rest[0] || args.id
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
result = await appApi('GET', `/campaigns/${campaignId}/metrics`)
break break
}
case 'trigger': { case 'trigger': {
const campaignId = rest[0] || args.id
if (!campaignId) { result = { error: 'Campaign ID required (positional arg or --id)' }; break }
const body = {} const body = {}
if (args.emails) body.emails = args.emails.split(',') if (args.emails) body.emails = args.emails.split(',')
if (args.ids) body.ids = args.ids.split(',') if (args.ids) body.ids = args.ids.split(',')
if (args.data) body.data = JSON.parse(args.data) if (args.data) body.data = JSON.parse(args.data)
result = await appApi('POST', `/campaigns/${rest[0]}/triggers`, body) result = await appApi('POST', `/campaigns/${campaignId}/triggers`, body)
break break
} }
default: default:

View file

@ -77,10 +77,13 @@ async function main() {
const params = new URLSearchParams() const params = new URLSearchParams()
if (args.domain) params.set('domain', args.domain) if (args.domain) params.set('domain', args.domain)
if (args.key) params.set('key', args.key) if (args.key) params.set('key', args.key)
result = await api('GET', `/links?${params}`) if (args['link-id']) params.set('linkId', args['link-id'])
if (args['external-id']) params.set('externalId', args['external-id'])
result = await api('GET', `/links/info?${params}`)
break break
} }
case 'update': { case 'update': {
if (!args.id) { result = { error: '--id required (link ID)' }; break }
const body = {} const body = {}
if (args.url) body.url = args.url if (args.url) body.url = args.url
if (args.tags) body.tags = args.tags.split(',') if (args.tags) body.tags = args.tags.split(',')
@ -88,6 +91,7 @@ async function main() {
break break
} }
case 'delete': case 'delete':
if (!args.id) { result = { error: '--id required (link ID)' }; break }
result = await api('DELETE', `/links/${args.id}`) result = await api('DELETE', `/links/${args.id}`)
break break
case 'bulk-create': { case 'bulk-create': {

View file

@ -155,7 +155,7 @@ async function main() {
operations: [{ operations: [{
update: { update: {
resourceName: `customers/${CUSTOMER_ID}/campaignBudgets/${args.id}`, resourceName: `customers/${CUSTOMER_ID}/campaignBudgets/${args.id}`,
amountMicros, amount_micros: amountMicros,
}, },
updateMask: 'amount_micros', updateMask: 'amount_micros',
}], }],

View file

@ -90,7 +90,7 @@ async function main() {
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body) result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
break break
case 'countries': case 'countries':
body.dimensions = ['country', 'query'] body.dimensions = ['country']
result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body) result = await api('POST', `/webmasters/v3/sites/${encodedSiteUrl}/searchAnalytics/query`, body)
break break
default: default:
@ -106,6 +106,7 @@ async function main() {
} }
switch (sub) { switch (sub) {
case 'url': case 'url':
if (!args.url) { result = { error: '--url required (URL to inspect)' }; break }
result = await api('POST', '/v1/urlInspection/index:inspect', { result = await api('POST', '/v1/urlInspection/index:inspect', {
inspectionUrl: args.url, inspectionUrl: args.url,
siteUrl: siteUrl, siteUrl: siteUrl,

View file

@ -2,7 +2,8 @@
const CLIENT_ID = process.env.HOTJAR_CLIENT_ID const CLIENT_ID = process.env.HOTJAR_CLIENT_ID
const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET const CLIENT_SECRET = process.env.HOTJAR_CLIENT_SECRET
const BASE_URL = 'https://api.hotjar.io/v1' const OAUTH_URL = 'https://api.hotjar.io'
const BASE_URL = 'https://api.hotjar.io/v2'
if (!CLIENT_ID || !CLIENT_SECRET) { if (!CLIENT_ID || !CLIENT_SECRET) {
console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' })) console.error(JSON.stringify({ error: 'HOTJAR_CLIENT_ID and HOTJAR_CLIENT_SECRET environment variables required' }))
@ -13,7 +14,7 @@ let cachedToken = null
async function getToken() { async function getToken() {
if (cachedToken) return cachedToken if (cachedToken) return cachedToken
const res = await fetch(`${BASE_URL}/oauth/token`, { const res = await fetch(`${OAUTH_URL}/oauth/token`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`, body: `grant_type=client_credentials&client_id=${encodeURIComponent(CLIENT_ID)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,

View file

@ -76,17 +76,19 @@ async function main() {
} }
case 'create': { case 'create': {
if (!args['account-id'] || !args.name) { result = { error: '--account-id and --name required' }; break } if (!args['account-id'] || !args.name) { result = { error: '--account-id and --name required' }; break }
if (!args['campaign-group-id']) { result = { error: '--campaign-group-id required' }; break }
const body = { const body = {
account: `urn:li:sponsoredAccount:${args['account-id']}`, account: `urn:li:sponsoredAccount:${args['account-id']}`,
campaignGroup: `urn:li:sponsoredCampaignGroup:${args['campaign-group-id']}`,
name: args.name, name: args.name,
type: args.type || 'SPONSORED_UPDATES', type: args.type || 'SPONSORED_UPDATES',
costType: args['cost-type'] || 'CPC', costType: args['cost-type'] || 'CPC',
unitCost: { unitCost: {
amount: args['unit-cost'] || '5.00', amount: parseFloat(args['unit-cost'] || '5.00'),
currencyCode: 'USD', currencyCode: 'USD',
}, },
dailyBudget: { dailyBudget: {
amount: args['daily-budget'] || '100.00', amount: parseFloat(args['daily-budget'] || '100.00'),
currencyCode: 'USD', currencyCode: 'USD',
}, },
status: 'PAUSED', status: 'PAUSED',

View file

@ -76,10 +76,14 @@ async function main() {
case 'referrals': case 'referrals':
switch (sub) { switch (sub) {
case 'get': case 'get': {
result = await api('GET', `/referral/${rest[0]}`) const refId = rest[0] || args.id
if (!refId) { result = { error: 'Referral ID required (positional arg or --id)' }; break }
result = await api('GET', `/referral/${refId}`)
break break
}
case 'list': { case 'list': {
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
result = await api('GET', `/referrer/${args['customer-id']}/referrals`) result = await api('GET', `/referrer/${args['customer-id']}/referrals`)
break break
} }
@ -91,6 +95,7 @@ async function main() {
case 'share-links': case 'share-links':
switch (sub) { switch (sub) {
case 'get': case 'get':
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
result = await api('GET', `/referrer/${args['customer-id']}/share-links`) result = await api('GET', `/referrer/${args['customer-id']}/share-links`)
break break
default: default:
@ -101,9 +106,11 @@ async function main() {
case 'rewards': case 'rewards':
switch (sub) { switch (sub) {
case 'get': case 'get':
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
result = await api('GET', `/referrer/${args['customer-id']}/rewards`) result = await api('GET', `/referrer/${args['customer-id']}/rewards`)
break break
case 'redeem': { case 'redeem': {
if (!args['customer-id']) { result = { error: '--customer-id required' }; break }
const body = {} const body = {}
if (args['reward-id']) body.reward_id = args['reward-id'] if (args['reward-id']) body.reward_id = args['reward-id']
if (args['order-number']) body.order_number = args['order-number'] if (args['order-number']) body.order_number = args['order-number']

View file

@ -135,7 +135,7 @@ async function main() {
case 'archive': { case 'archive': {
const id = args.id const id = args.id
if (!id) { result = { error: '--id required' }; break } if (!id) { result = { error: '--id required' }; break }
result = await api('DELETE', `/experiments/${id}`) result = await api('PATCH', `/experiments/${id}`, { status: 'archived' })
break break
} }
default: default:

View file

@ -202,8 +202,9 @@ async function main() {
result = await api('GET', `/webhooks/${rest[0]}`) result = await api('GET', `/webhooks/${rest[0]}`)
break break
case 'create': { case 'create': {
if (!args.url) { result = { error: '--url required (webhook URL)' }; break }
const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced'] const events = args.events?.split(',') || ['email.sent', 'email.delivered', 'email.bounced']
result = await api('POST', '/webhooks', { endpoint: args.endpoint, events }) result = await api('POST', '/webhooks', { url: args.url, events })
break break
} }
case 'delete': case 'delete':

View file

@ -9,13 +9,14 @@ if (!API_KEY) {
} }
async function api(method, path, body) { async function api(method, path, body) {
const auth = 'Basic ' + Buffer.from(`${API_KEY}:`).toString('base64')
if (args['dry-run']) { if (args['dry-run']) {
return { _dry_run: true, method, url: `${BASE_URL}${path}`, headers: { Authorization: '***', 'Content-Type': 'application/json' }, body: body || undefined } 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}`, { const res = await fetch(`${BASE_URL}${path}`, {
method, method,
headers: { headers: {
'Authorization': `Bearer ${API_KEY}`, 'Authorization': auth,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,

View file

@ -148,10 +148,10 @@ async function main() {
case 'prospects': { case 'prospects': {
const id = args.id const id = args.id
if (!id) { result = { error: '--id required' }; break } if (!id) { result = { error: '--id required' }; break }
const params = new URLSearchParams({ listId: id }) const body = { listId: id }
if (args.page) params.set('page', args.page) if (args.page) body.page = Number(args.page)
if (args['per-page']) params.set('perPage', args['per-page']) if (args['per-page']) body.perPage = Number(args['per-page'])
result = await api('GET', `/prospect-list?${params.toString()}`) result = await api('POST', '/prospect-list', body)
break break
} }
default: default:

View file

@ -60,9 +60,12 @@ async function main() {
case 'list': case 'list':
result = await api('GET', '/affiliates') result = await api('GET', '/affiliates')
break break
case 'get': case 'get': {
result = await api('GET', `/affiliates/${rest[0]}`) const id = rest[0] || args.id
if (!id) { result = { error: 'Affiliate ID required (positional arg or --id)' }; break }
result = await api('GET', `/affiliates/${id}`)
break break
}
case 'create': { case 'create': {
const body = {} const body = {}
if (args.email) body.email = args.email if (args.email) body.email = args.email
@ -71,6 +74,7 @@ async function main() {
break break
} }
case 'update': { case 'update': {
if (!args.id) { result = { error: '--id required (affiliate ID)' }; break }
const body = {} const body = {}
if (args['commission-rate']) body.commission_rate = Number(args['commission-rate']) if (args['commission-rate']) body.commission_rate = Number(args['commission-rate'])
if (args['payout-method']) body.payout_method = args['payout-method'] if (args['payout-method']) body.payout_method = args['payout-method']