collabSupport/integrations/cisco-mpp-phone/client.js
jmcqueen f7953b8eb5 Add MPP desk phone diagnostics follow-up to /phonestatus via relay.
Wire CP-78xx probe discovery, relay phone-probe commands, and a chat follow-up message so store desk phones get registration, switch, and provisioning detail alongside DECT and WAN diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 18:01:01 -04:00

123 lines
3.7 KiB
JavaScript

// integrations/cisco-mpp-phone/client.js
//
// Axios wrapper for Cisco MPP desk phones (CP-78xx, Webex Calling).
// HTTPS on 443 with a self-signed cert; Basic auth is typical, with
// Digest retry on 401 (same interceptor pattern as DECT).
import axios from 'axios';
import https from 'node:https';
import {
parseDigestChallenge,
buildDigestAuthHeader,
} from '../../utils/httpDigestAuth.js';
/**
* @param {object} opts
* @param {string} opts.host
* @param {string} [opts.user] default `admin` when password is set
* @param {string} [opts.password] omit for unauthenticated probes (Webex MPP JSON)
* @param {number} [opts.timeoutMs]
* @returns {import('axios').AxiosInstance}
*/
export function createMppPhoneClient({ host, user, password, timeoutMs = 15_000 }) {
if (!host) throw new Error('createMppPhoneClient: host is required');
const useAuth = !!(password && String(password).length > 0);
const authUser = user || 'admin';
const client = axios.create({
baseURL: `https://${host}`,
timeout: timeoutMs,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
validateStatus: () => true,
responseType: 'text',
transformResponse: [(data) => data],
...(useAuth ? { auth: { username: authUser, password } } : {}),
headers: { 'User-Agent': 'collabSupport-mpp-phone/0.1' },
});
if (useAuth) {
client.defaults.__mppAuth = { user: authUser, password };
}
client.interceptors.response.use(async (response) => {
if (response.status !== 401) return response;
if (!client.defaults.__mppAuth) return response;
const originalConfig = response.config;
if (originalConfig.__digestRetried) return response;
const wwwAuth = response.headers?.['www-authenticate'];
const challenge = parseDigestChallenge(wwwAuth);
if (!challenge) return response;
const { user: username, password: pass } = client.defaults.__mppAuth;
const method = (originalConfig.method || 'get').toUpperCase();
const uri = originalConfig.url || '/';
const authHeader = buildDigestAuthHeader({
username, password: pass, method, uri, challenge,
});
return client.request({
...originalConfig,
auth: undefined,
headers: { ...(originalConfig.headers || {}), Authorization: authHeader },
__digestRetried: true,
});
});
return client;
}
/**
* @typedef {object} ProbeResult
* @property {string} path
* @property {string} method
* @property {number|null} status
* @property {string|null} contentType
* @property {number} sizeBytes
* @property {string|null} snippet
* @property {string|null} error
* @property {number} elapsedMs
*/
export async function tryRequest(client, { method = 'GET', path, data, headers } = {}) {
const start = Date.now();
try {
const res = await client.request({ method, url: path, data, headers });
return normalize({
path, method,
status: res.status,
contentType: res.headers?.['content-type'] || null,
body: res.data,
elapsedMs: Date.now() - start,
});
} catch (err) {
return {
path,
method,
status: null,
contentType: null,
sizeBytes: 0,
snippet: null,
error: err.code ? `${err.code}: ${err.message}` : err.message,
elapsedMs: Date.now() - start,
};
}
}
function normalize({ path, method, status, contentType, body, elapsedMs, error = null }) {
const bodyStr = typeof body === 'string' ? body : (body == null ? '' : String(body));
const flat = bodyStr.replace(/\s+/g, ' ').trim();
return {
path,
method,
status,
contentType,
sizeBytes: Buffer.byteLength(bodyStr, 'utf8'),
snippet: flat ? flat.slice(0, 200) : null,
error,
elapsedMs,
};
}