#!/usr/bin/env node /** * Seed the Webex Service App tokens file from an existing source. * * Two seed paths, both end with one immediate refresh against * https://webexapis.com/v1/access_token. That verifies the seed actually * works, rotates to a fresh pair (Cisco rotates refresh tokens), and writes * the rotated pair to this project's WEBEX_TOKENS_PATH. * * Usage: * npm run webex:seed # uses default --from-file path (collabFinder) * npm run webex:seed -- --from-file /path/to/tokens.json * npm run webex:seed -- --refresh-token * * Requires WEBEX_CLIENT_ID + WEBEX_CLIENT_SECRET in .env. */ require('dotenv').config(); const fs = require('fs').promises; const path = require('path'); const axios = require('axios'); const TOKEN_URL = 'https://webexapis.com/v1/access_token'; const DEFAULT_SOURCE = '/Volumes/jmcqueen/Docker/collabFinder/config/webex-service-tokens.json'; const SAFETY_BUFFER_MS = 5 * 60 * 1000; const args = process.argv.slice(2); function flag(name) { const idx = args.indexOf(name); if (idx >= 0 && idx + 1 < args.length) return args[idx + 1]; const inline = args.find(a => a.startsWith(`${name}=`)); if (inline) return inline.split('=').slice(1).join('='); return null; } async function readSeedRefreshToken() { const refreshTokenFlag = flag('--refresh-token'); if (refreshTokenFlag) { return { source: 'refresh-token flag', refreshToken: refreshTokenFlag.trim() }; } const fromFile = flag('--from-file') || DEFAULT_SOURCE; console.log(`Seeding from file: ${fromFile}`); let raw; try { raw = await fs.readFile(fromFile, 'utf8'); } catch (err) { if (err.code === 'ENOENT') { throw new Error( `Source tokens file not found: ${fromFile}\n` + 'Pass --from-file or --refresh-token .', { cause: err } ); } throw err; } let parsed; try { parsed = JSON.parse(raw); } catch (err) { throw new Error(`Source tokens file is not valid JSON (${fromFile}): ${err.message}`, { cause: err, }); } const refreshToken = parsed.refreshToken || parsed.refresh_token; if (!refreshToken) { throw new Error(`Source tokens file has no refreshToken / refresh_token field (${fromFile})`); } return { source: fromFile, refreshToken }; } async function exchangeRefreshToken(refreshToken) { const clientId = process.env.WEBEX_CLIENT_ID; const clientSecret = process.env.WEBEX_CLIENT_SECRET; if (!clientId || !clientSecret) { throw new Error('WEBEX_CLIENT_ID and WEBEX_CLIENT_SECRET must be set in .env'); } const params = new URLSearchParams({ grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret, refresh_token: refreshToken, }); try { const res = await axios.post(TOKEN_URL, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 15000, }); return res.data; } catch (err) { const status = err.response?.status; const detail = err.response?.data ? JSON.stringify(err.response.data) : err.message; throw new Error(`Token refresh failed (status=${status || 'n/a'}): ${detail}`, { cause: err, }); } } async function writeTokens(targetPath, data) { const payload = { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + Number(data.expires_in || 0) * 1000 - SAFETY_BUFFER_MS, updatedAt: new Date().toISOString(), }; await fs.mkdir(path.dirname(targetPath), { recursive: true }); await fs.writeFile(targetPath, JSON.stringify(payload, null, 2), 'utf8'); return payload; } (async () => { try { const targetPath = path.resolve( process.env.WEBEX_TOKENS_PATH || path.join(process.cwd(), 'tokens', 'webex-service-tokens.json') ); const { source, refreshToken } = await readSeedRefreshToken(); console.log(`Seed source: ${source}`); console.log(`Target path: ${targetPath}`); console.log('Performing initial refresh against Webex...'); const data = await exchangeRefreshToken(refreshToken); const payload = await writeTokens(targetPath, data); console.log('Webex tokens seeded successfully.'); console.log(` expiresIn: ${data.expires_in}s (buffered)`); console.log(` expiresAt: ${new Date(payload.expiresAt).toISOString()}`); console.log(` updatedAt: ${payload.updatedAt}`); process.exit(0); } catch (err) { console.error('Seed failed:', err.message); process.exit(1); } })();