/** * Quick connectivity test for both Duo API integrations. * Calls a lightweight read endpoint on each: * Accounts API → GET /admin/v1/accounts (lists child accounts) * Admin API → GET /admin/v1/info/summary (tenant summary stats) * * Duo auth: HMAC-SHA1 signed "Authorization: Basic " header * https://duo.com/docs/adminapi#authentication */ import crypto from 'crypto'; import https from 'https'; // ── credentials from .env.local ────────────────────────────────────────────── const ACCOUNTS = { ikey: process.env.DUOACCOUNTS_INTEGRATION_KEY, skey: process.env.DUOACCOUNTS_SECRET_KEY, host: process.env.DUOACCOUNTS_API_HOSTNAME, }; const ADMIN = { ikey: process.env.DUOADMIN_INTEGRATION_KEY, skey: process.env.DUOADMIN_SECRET_KEY, host: process.env.DUOADMIN_API_HOSTNAME, }; // ── Duo HMAC signer ─────────────────────────────────────────────────────────── function duoSign(ikey, skey, host, method, path, params = {}) { const date = new Date().toUTCString(); const sortedParams = Object.keys(params) .sort() .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&'); const canon = [date, method.toUpperCase(), host.toLowerCase(), path, sortedParams].join('\n'); const sig = crypto.createHmac('sha1', skey).update(canon).digest('hex'); const auth = Buffer.from(`${ikey}:${sig}`).toString('base64'); return { date, auth: `Basic ${auth}`, query: sortedParams }; } // ── generic HTTPS GET ───────────────────────────────────────────────────────── function duoGet(creds, path, params = {}) { return new Promise((resolve, reject) => { const { date, auth, query } = duoSign(creds.ikey, creds.skey, creds.host, 'GET', path, params); const url = `https://${creds.host}${path}${query ? '?' + query : ''}`; const req = https.get(url, { headers: { Authorization: auth, Date: date, 'Content-Type': 'application/json' }, }, res => { let body = ''; res.on('data', d => body += d); res.on('end', () => { try { resolve({ status: res.statusCode, data: JSON.parse(body) }); } catch { resolve({ status: res.statusCode, data: body }); } }); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); }); } // ── run tests ──────────────────────────────────────────────────────────────── async function main() { console.log('=== Duo API Connectivity Test ===\n'); // Validate env for (const [name, val] of Object.entries({ ...ACCOUNTS, ...ADMIN })) { if (!val) { console.error(`Missing env var for key: ${name}`); process.exit(1); } } // 1. Accounts API — POST /accounts/v1/account/list (all Accounts API endpoints use POST) console.log('── Accounts API ────────────────────────────────'); console.log(`Host : ${ACCOUNTS.host}`); console.log(`IKey : ${ACCOUNTS.ikey}`); try { const r = await duoPost(ACCOUNTS, '/accounts/v1/account/list'); console.log(`HTTP : ${r.status}`); if (r.status === 200) { const accounts = r.data?.response ?? []; console.log(`✓ OK — ${accounts.length} child account(s) returned`); accounts.slice(0, 5).forEach(a => console.log(` · ${a.name} (${a.account_id}) — ${a.api_hostname}`)); } else { console.log(`✗ Response:`, JSON.stringify(r.data, null, 2)); } } catch (e) { console.log(`✗ Error: ${e.message}`); } console.log(); // 2. Admin API — GET /admin/v1/info/summary console.log('── Admin API ───────────────────────────────────'); console.log(`Host : ${ADMIN.host}`); console.log(`IKey : ${ADMIN.ikey}`); try { const r = await duoGet(ADMIN, '/admin/v1/info/summary'); console.log(`HTTP : ${r.status}`); if (r.status === 200) { const s = r.data?.response ?? {}; console.log('✓ OK — Summary:'); console.log(` Users : ${s.user_count ?? 'n/a'}`); console.log(` Integrations : ${s.integration_count ?? 'n/a'}`); console.log(` Phones : ${s.telephony_credits_remaining ?? 'n/a'} credits remaining`); } else { console.log(`✗ Response:`, JSON.stringify(r.data, null, 2)); } } catch (e) { console.log(`✗ Error: ${e.message}`); } // 3. Admin API — GET /admin/v1/users (first page, limit 5) console.log(); console.log('── Admin API — Users (first 5) ─────────────────'); try { const r = await duoGet(ADMIN, '/admin/v1/users', { limit: '5', offset: '0' }); console.log(`HTTP : ${r.status}`); if (r.status === 200) { const users = r.data?.response ?? []; console.log(`✓ OK — ${users.length} user(s) in page`); users.forEach(u => console.log(` · ${u.username} — ${u.status} — ${u.email ?? '(no email)'}`)); } else { console.log(`✗ Response:`, JSON.stringify(r.data, null, 2)); } } catch (e) { console.log(`✗ Error: ${e.message}`); } console.log('\n=== Done ==='); } main();