#!/usr/bin/env node /** * Cleanup stale Webex Device Manager (WDM) registrations for the bot token. * * Webex caps each user/bot at a fixed number of device registrations * (currently ~100). Every time webex-node-bot-framework starts, it registers * a new device; if the process is killed before framework.stop() runs (e.g. * by nodemon SIGKILL, OOM, or a crash), the registration is orphaned. * Once you hit the cap, new logins fail with: * "User has excessive device registrations" * * Usage: * node scripts/cleanupWebexDevices.js # dry-run (lists only) * node scripts/cleanupWebexDevices.js --delete # actually delete them * node scripts/cleanupWebexDevices.js --delete --keep-newest=1 * * Requires WEBEX_ACCESS_TOKEN in .env. */ require('dotenv').config(); const axios = require('axios'); const WDM_BASE = 'https://wdm-a.wbx2.com/wdm/api/v1'; const TOKEN = process.env.WEBEX_ACCESS_TOKEN; const args = process.argv.slice(2); const doDelete = args.includes('--delete'); const keepNewestArg = args.find(a => a.startsWith('--keep-newest=')); const keepNewest = keepNewestArg ? parseInt(keepNewestArg.split('=')[1], 10) || 0 : 0; if (!TOKEN) { console.error('❌ WEBEX_ACCESS_TOKEN is not set in .env'); process.exit(1); } const api = axios.create({ baseURL: WDM_BASE, headers: { Authorization: `Bearer ${TOKEN}` }, timeout: 15000, }); function fmtDate(s) { if (!s) return 'unknown'; try { return new Date(s).toISOString(); } catch (_e) { return String(s); } } async function listDevices() { try { const res = await api.get('/devices'); const devices = res.data?.devices || res.data || []; return Array.isArray(devices) ? devices : []; } catch (err) { const status = err.response?.status; const body = err.response?.data; console.error('❌ Failed to list devices', { status, error: err.message, body }); process.exit(1); } } async function deleteDevice(device) { // The WDM API returns either a `url` (full URL) or a `deviceUrl`. Prefer the // explicit URL; otherwise fall back to /devices/{id}. const url = device.url || device.deviceUrl; try { if (url) { await axios.delete(url, { headers: { Authorization: `Bearer ${TOKEN}` }, timeout: 15000 }); } else if (device.id) { await api.delete(`/devices/${device.id}`); } else { throw new Error('device has no url or id; skipping'); } return { ok: true }; } catch (err) { return { ok: false, error: err.response?.data || err.message }; } } (async function main() { const devices = await listDevices(); console.log(`Found ${devices.length} device registration(s) for this token.\n`); if (devices.length === 0) { console.log('Nothing to clean up. ✅'); return; } // Sort newest → oldest by modificationTime / creationTime so --keep-newest // keeps the most recently-touched registrations. const sorted = [...devices].sort((a, b) => { const ta = new Date(a.modificationTime || a.creationTime || 0).getTime(); const tb = new Date(b.modificationTime || b.creationTime || 0).getTime(); return tb - ta; }); sorted.forEach((d, i) => { console.log( [ `[${i}]`, d.deviceType || 'unknown-type', `name=${d.name || d.userAgent || 'n/a'}`, `created=${fmtDate(d.creationTime)}`, `modified=${fmtDate(d.modificationTime)}`, `id=${d.id || (d.url || '').split('/').pop()}`, ].join(' ') ); }); const toDelete = sorted.slice(keepNewest); if (!doDelete) { console.log(`\nDry run — would delete ${toDelete.length} device(s).`); console.log(`Re-run with --delete to actually remove them.`); if (keepNewest > 0) { console.log(`(Keeping the ${keepNewest} newest registration(s).)`); } return; } console.log(`\nDeleting ${toDelete.length} device(s)...`); let okCount = 0; let failCount = 0; for (const d of toDelete) { const result = await deleteDevice(d); const tag = `[${d.id || (d.url || '').split('/').pop()}]`; if (result.ok) { okCount++; console.log(` ✅ deleted ${tag}`); } else { failCount++; console.log(` ❌ failed ${tag} ${JSON.stringify(result.error)}`); } } console.log(`\nDone. Deleted ${okCount}, failed ${failCount}.`); if (keepNewest > 0) { console.log(`(Kept the ${keepNewest} newest registration(s).)`); } })();