fix: security hardening — move meta-ads to header auth, encode URLs
Critical: - meta-ads: move access_token from URL query string to Authorization header to prevent credential leakage in server logs and referrers Medium (URL encoding): - g2: encode state and date filter values - trustpilot: use URLSearchParams for reviews list params - typeform: encode response IDs in delete endpoint - demio: encode event type filter - lemlist: encode email addresses in URL path segments Docs: - Fix 6 missing env vars in CLI README auth table - Fix .gitignore typo (extra space in .DS_Store pattern) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
47b4571ca2
commit
c1be574c8b
8 changed files with 31 additions and 29 deletions
|
|
@ -37,7 +37,7 @@ Every CLI reads credentials from environment variables:
|
|||
| CLI | Environment Variable |
|
||||
|-----|---------------------|
|
||||
| `activecampaign` | `ACTIVECAMPAIGN_API_KEY`, `ACTIVECAMPAIGN_API_URL` |
|
||||
| `adobe-analytics` | `ADOBE_CLIENT_ID`, `ADOBE_ACCESS_TOKEN` |
|
||||
| `adobe-analytics` | `ADOBE_ACCESS_TOKEN`, `ADOBE_CLIENT_ID`, `ADOBE_COMPANY_ID` |
|
||||
| `ahrefs` | `AHREFS_API_KEY` |
|
||||
| `amplitude` | `AMPLITUDE_API_KEY`, `AMPLITUDE_SECRET_KEY` |
|
||||
| `apollo` | `APOLLO_API_KEY` |
|
||||
|
|
@ -52,7 +52,7 @@ Every CLI reads credentials from environment variables:
|
|||
| `dub` | `DUB_API_KEY` |
|
||||
| `g2` | `G2_API_TOKEN` |
|
||||
| `ga4` | `GA4_ACCESS_TOKEN` |
|
||||
| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN` |
|
||||
| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` |
|
||||
| `google-search-console` | `GSC_ACCESS_TOKEN` |
|
||||
| `hotjar` | `HOTJAR_CLIENT_ID`, `HOTJAR_CLIENT_SECRET` |
|
||||
| `intercom` | `INTERCOM_API_KEY` |
|
||||
|
|
@ -63,13 +63,13 @@ Every CLI reads credentials from environment variables:
|
|||
| `livestorm` | `LIVESTORM_API_TOKEN` |
|
||||
| `mailchimp` | `MAILCHIMP_API_KEY` |
|
||||
| `mention-me` | `MENTIONME_API_KEY` |
|
||||
| `meta-ads` | `META_ACCESS_TOKEN` |
|
||||
| `meta-ads` | `META_ACCESS_TOKEN`, `META_AD_ACCOUNT_ID` |
|
||||
| `mixpanel` | `MIXPANEL_TOKEN` (ingestion), `MIXPANEL_API_KEY` + `MIXPANEL_SECRET` (query) |
|
||||
| `onesignal` | `ONESIGNAL_REST_API_KEY`, `ONESIGNAL_APP_ID` |
|
||||
| `optimizely` | `OPTIMIZELY_API_KEY` |
|
||||
| `paddle` | `PADDLE_API_KEY` |
|
||||
| `paddle` | `PADDLE_API_KEY`, `PADDLE_SANDBOX` (optional) |
|
||||
| `partnerstack` | `PARTNERSTACK_PUBLIC_KEY`, `PARTNERSTACK_SECRET_KEY` |
|
||||
| `plausible` | `PLAUSIBLE_API_KEY` |
|
||||
| `plausible` | `PLAUSIBLE_API_KEY`, `PLAUSIBLE_BASE_URL` (optional, for self-hosted) |
|
||||
| `postmark` | `POSTMARK_API_KEY` |
|
||||
| `resend` | `RESEND_API_KEY` |
|
||||
| `rewardful` | `REWARDFUL_API_KEY` |
|
||||
|
|
@ -77,7 +77,7 @@ Every CLI reads credentials from environment variables:
|
|||
| `segment` | `SEGMENT_WRITE_KEY` (tracking), `SEGMENT_ACCESS_TOKEN` (profile) |
|
||||
| `semrush` | `SEMRUSH_API_KEY` |
|
||||
| `sendgrid` | `SENDGRID_API_KEY` |
|
||||
| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN` |
|
||||
| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN`, `TIKTOK_ADVERTISER_ID` |
|
||||
| `tolt` | `TOLT_API_KEY` |
|
||||
| `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` |
|
||||
| `typeform` | `TYPEFORM_API_KEY` |
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ async function main() {
|
|||
case 'list': {
|
||||
const type = args.type
|
||||
let qs = ''
|
||||
if (type) qs = `?type=${type}`
|
||||
if (type) qs = `?type=${encodeURIComponent(type)}`
|
||||
result = await api('GET', `/events${qs}`)
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ async function main() {
|
|||
case 'list': {
|
||||
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||
if (productId) qs += `&filter[product_id]=${productId}`
|
||||
if (args.state) qs += `&filter[state]=${args.state}`
|
||||
if (args.state) qs += `&filter[state]=${encodeURIComponent(args.state)}`
|
||||
result = await api('GET', `/survey-responses${qs}`)
|
||||
break
|
||||
}
|
||||
|
|
@ -152,8 +152,8 @@ async function main() {
|
|||
switch (sub) {
|
||||
case 'visitors': {
|
||||
let qs = `?page[size]=${perPage}&page[number]=${page}`
|
||||
if (args.start) qs += `&filter[start_date]=${args.start}`
|
||||
if (args.end) qs += `&filter[end_date]=${args.end}`
|
||||
if (args.start) qs += `&filter[start_date]=${encodeURIComponent(args.start)}`
|
||||
if (args.end) qs += `&filter[end_date]=${encodeURIComponent(args.end)}`
|
||||
result = await api('GET', `/tracking-events${qs}`)
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ async function main() {
|
|||
case 'get': {
|
||||
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||
if (!args.email) { result = { error: '--email required' }; break }
|
||||
result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${args.email}`)
|
||||
result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`)
|
||||
break
|
||||
}
|
||||
case 'add': {
|
||||
|
|
@ -122,13 +122,13 @@ async function main() {
|
|||
if (args['first-name']) body.firstName = args['first-name']
|
||||
if (args['last-name']) body.lastName = args['last-name']
|
||||
if (args.company) body.companyName = args.company
|
||||
result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${args.email}`, body)
|
||||
result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`, body)
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break }
|
||||
if (!args.email) { result = { error: '--email required' }; break }
|
||||
result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${args.email}`)
|
||||
result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
|
@ -147,12 +147,12 @@ async function main() {
|
|||
}
|
||||
case 'add': {
|
||||
if (!args.email) { result = { error: '--email required' }; break }
|
||||
result = await api('POST', `/unsubscribes/${args.email}`)
|
||||
result = await api('POST', `/unsubscribes/${encodeURIComponent(args.email)}`)
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
if (!args.email) { result = { error: '--email required' }; break }
|
||||
result = await api('DELETE', `/unsubscribes/${args.email}`)
|
||||
result = await api('DELETE', `/unsubscribes/${encodeURIComponent(args.email)}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@ if (!TOKEN) {
|
|||
}
|
||||
|
||||
async function api(method, path, body) {
|
||||
const separator = path.includes('?') ? '&' : '?'
|
||||
const url = `${BASE_URL}${path}${separator}access_token=${TOKEN}`
|
||||
const opts = { method, headers: {} }
|
||||
const url = `${BASE_URL}${path}`
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Authorization': `Bearer ${TOKEN}` },
|
||||
}
|
||||
if (body) {
|
||||
opts.headers['Content-Type'] = 'application/json'
|
||||
opts.body = JSON.stringify(body)
|
||||
}
|
||||
if (args['dry-run']) {
|
||||
const dryRunUrl = url.replace(TOKEN, '***')
|
||||
return { _dry_run: true, method, url: dryRunUrl, headers: opts.headers, body: body || undefined }
|
||||
return { _dry_run: true, method, url, headers: { ...opts.headers, Authorization: '***' }, body: body || undefined }
|
||||
}
|
||||
const res = await fetch(url, opts)
|
||||
const text = await res.text()
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ async function main() {
|
|||
case 'web-links': {
|
||||
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||
const locale = args.locale || 'en-US'
|
||||
result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${locale}`)
|
||||
result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${encodeURIComponent(locale)}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
|
@ -134,10 +134,10 @@ async function main() {
|
|||
switch (sub) {
|
||||
case 'list': {
|
||||
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||
const stars = args.stars ? `&stars=${args.stars}` : ''
|
||||
const lang = args.language ? `&language=${args.language}` : ''
|
||||
const orderBy = args['order-by'] || 'createdat.desc'
|
||||
result = await api('GET', `/business-units/${businessUnitId}/reviews?perPage=${limit}&orderBy=${orderBy}${stars}${lang}`)
|
||||
const reviewParams = new URLSearchParams({ perPage: String(limit), orderBy: args['order-by'] || 'createdat.desc' })
|
||||
if (args.stars) reviewParams.set('stars', args.stars)
|
||||
if (args.language) reviewParams.set('language', args.language)
|
||||
result = await api('GET', `/business-units/${businessUnitId}/reviews?${reviewParams}`)
|
||||
break
|
||||
}
|
||||
case 'get': {
|
||||
|
|
@ -148,8 +148,9 @@ async function main() {
|
|||
}
|
||||
case 'private': {
|
||||
if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break }
|
||||
const stars = args.stars ? `&stars=${args.stars}` : ''
|
||||
result = await api('GET', `/private/business-units/${businessUnitId}/reviews?perPage=${limit}${stars}`, null, 'bearer')
|
||||
const privateParams = new URLSearchParams({ perPage: String(limit) })
|
||||
if (args.stars) privateParams.set('stars', args.stars)
|
||||
result = await api('GET', `/private/business-units/${businessUnitId}/reviews?${privateParams}`, null, 'bearer')
|
||||
break
|
||||
}
|
||||
case 'latest':
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ async function main() {
|
|||
if (!id) { result = { error: '--id required (form ID)' }; break }
|
||||
const responseIds = args['response-ids']
|
||||
if (!responseIds) { result = { error: '--response-ids required (comma-separated)' }; break }
|
||||
result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${responseIds}`)
|
||||
result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${encodeURIComponent(responseIds)}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
Loading…
Reference in a new issue