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>
123 lines
3.7 KiB
JavaScript
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,
|
|
};
|
|
}
|