homebridge-adt-pulse
Version:
Homebridge security system platform for ADT Pulse
1,131 lines • 59.9 kB
JavaScript
import axios from 'axios';
import { wrapper } from 'axios-cookiejar-support';
import { JSDOM } from 'jsdom';
import _ from 'lodash';
import { isErrorLike, serializeError } from 'serialize-error';
import { CookieJar } from 'tough-cookie';
import { detectGlobalDebugParser, detectGlobalPortalVersion } from './detect.js';
import { generateFakeDynatracePCHeaderValue, generateFakeLoginFingerprint } from './fake.js';
import { objectKeyClientType, objectKeyLocale, objectKeyLogin, objectKeyPreAuthToken, objectKeySat, requestPathAccessSignIn, requestPathAccessSignInEXxPartnerAdt, requestPathMfaMfaSignInWorkflowChallenge, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthAddTrustedDeviceSatXx, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthRequestOtpForRegisteredPropertySatXx, requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthValidateOtpSatXx, requestPathNgaServRunRraProxyHrefRestIcontrolUiClientMultiFactorAuthSatXx, requestPathNgaServRunRraProxyOnlyClientMultiFactorAuthExcludeSatXxHrefRestAdtUiUpdatesSatXx, requestPathSummarySummary, requestPathSystemSystem, textOneTimePasscode, } from './regex.js';
import { multiFactorAuth, otpResponse } from './schema.js';
import { debugLog, fetchErrorMessage, findNullKeys, generateHash, parseMultiFactorMethods, parseMultiFactorTrustedDevices, parseSensorsTable, stackTracer, } from './utility.js';
export class ADTPulseAuth {
#connection;
#credentials;
#internal;
#session;
constructor(config, internalConfig) {
this.#connection = {
subdomain: config.subdomain,
};
this.#credentials = {
fingerprint: generateFakeLoginFingerprint(),
password: config.password,
username: config.username,
};
this.#internal = {
baseUrl: internalConfig.baseUrl ?? `https://${this.#connection.subdomain}.adtpulse.com`,
debug: internalConfig.debug ?? false,
logger: internalConfig.logger ?? null,
reportedHashes: [],
};
this.#session = {
httpClient: wrapper(axios.create({
jar: new CookieJar(),
validateStatus: () => true,
})),
mfa: {
clientType: null,
locale: null,
login: null,
preAuthToken: null,
satCode: null,
token: null,
trustedDevices: [],
verificationMethods: [],
},
portalVersion: null,
status: 'logged-out',
};
}
async getVerificationMethods() {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Attempting to retrieve verification methods from "${this.#internal.baseUrl}"`);
}
try {
const sessions = {};
if (this.#session.status === 'complete'
|| this.#session.status === 'not-required') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', 'Already retrieved verification methods');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: true,
info: {
methods: this.#session.mfa.verificationMethods,
status: this.#session.status,
},
};
}
sessions.axiosIndex = await this.#session.httpClient.get(`${this.#internal.baseUrl}/`, this.getRequestConfig());
if (sessions.axiosIndex.status >= 400) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`);
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `The remote server responded with a HTTP ${sessions.axiosIndex.status} status code`,
},
};
}
if (typeof sessions.axiosIndex?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosIndexRequestPath = sessions.axiosIndex.request.path;
const axiosIndexRequestPathValid = requestPathAccessSignIn.test(axiosIndexRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosIndexRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosIndexRequestPathValid}`);
}
if (!axiosIndexRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosIndexRequestPath} is not the sign-in page`);
}
this.handleLoginFailure(axiosIndexRequestPath, sessions.axiosIndex);
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `"${axiosIndexRequestPath} is not the sign-in page`,
},
};
}
const loginForm = new URLSearchParams();
loginForm.append('usernameForm', this.#credentials.username);
loginForm.append('passwordForm', this.#credentials.password);
loginForm.append('sun', 'yes');
loginForm.append('networkid', '');
loginForm.append('fingerprint', this.#credentials.fingerprint);
this.#session.portalVersion = axiosIndexRequestPath.replace(requestPathAccessSignIn, '$2');
await this.newInformationDispatcher('portal-version', { version: this.#session.portalVersion });
sessions.axiosSignIn = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`, loginForm, this.getRequestConfig({
headers: {
'Cache-Control': 'max-age=0',
'Content-Type': 'application/x-www-form-urlencoded',
Origin: this.#internal.baseUrl,
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/signin.jsp?e=ns&partner=adt`,
'Sec-Fetch-Site': 'same-origin',
},
}));
if (sessions.axiosSignIn.status >= 400) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosSignIn.status} status code`);
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `The remote server responded with a HTTP ${sessions.axiosSignIn.status} status code`,
},
};
}
if (typeof sessions.axiosSignIn?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosSignInRequestPath = sessions.axiosSignIn.request.path;
const axiosSignInRequestPathValid = requestPathMfaMfaSignInWorkflowChallenge.test(axiosSignInRequestPath) || requestPathSummarySummary.test(axiosSignInRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosSignInRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosSignInRequestPathValid}`);
}
if (requestPathSummarySummary.test(axiosSignInRequestPath)) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Verification methods not required from "${this.#internal.baseUrl}"`);
}
this.#session.status = 'not-required';
return {
action: 'GET_VERIFICATION_METHODS',
success: true,
info: {
methods: this.#session.mfa.verificationMethods,
status: this.#session.status,
},
};
}
if (requestPathAccessSignIn.test(axiosSignInRequestPath) || requestPathAccessSignInEXxPartnerAdt.test(axiosSignInRequestPath)) {
const errorMessage = fetchErrorMessage(sessions.axiosSignIn);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', errorMessage ?? 'Unknown error');
}
this.#session.status = 'logged-out';
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: errorMessage ?? 'Unknown error',
},
};
}
if (!axiosSignInRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosSignInRequestPath}" is not the workflow challenge page`);
}
this.handleLoginFailure(axiosSignInRequestPath, sessions.axiosSignIn);
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `"${axiosSignInRequestPath}" is not the workflow challenge page`,
},
};
}
if (typeof sessions.axiosSignIn.data !== 'string') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The response body of the workflow challenge page is not of type "string"');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'The response body of the workflow challenge page is not of type "string"',
},
};
}
const matchClientType = sessions.axiosSignIn.data.match(objectKeyClientType);
const matchLocale = sessions.axiosSignIn.data.match(objectKeyLocale);
const matchLogin = sessions.axiosSignIn.data.match(objectKeyLogin);
const matchPreAuthToken = sessions.axiosSignIn.data.match(objectKeyPreAuthToken);
const matchSatCode = sessions.axiosSignIn.data.match(objectKeySat);
this.#session.mfa.clientType = (matchClientType !== null && matchClientType.length >= 2) ? matchClientType[1] : null;
this.#session.mfa.locale = (matchLocale !== null && matchLocale.length >= 2) ? matchLocale[1] : null;
this.#session.mfa.login = (matchLogin !== null && matchLogin.length >= 2) ? matchLogin[1] : null;
this.#session.mfa.preAuthToken = (matchPreAuthToken !== null && matchPreAuthToken.length >= 2) ? matchPreAuthToken[1] : null;
this.#session.mfa.satCode = (matchSatCode !== null && matchSatCode.length >= 2) ? matchSatCode[1] : null;
if (this.#session.mfa.clientType === null
|| this.#session.mfa.locale === null
|| this.#session.mfa.login === null
|| this.#session.mfa.preAuthToken === null
|| this.#session.mfa.satCode === null) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Failed to retrieve required MFA details from the workflow challenge page');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'Failed to retrieve required MFA details from the workflow challenge page',
},
};
}
sessions.axiosMethods = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/icontrol/ui/client/multiFactorAuth&sat=${this.#session.mfa.satCode}`, this.getRequestConfig({
headers: {
Accept: 'application/json',
'Content-Type': undefined,
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': undefined,
'Upgrade-Insecure-Requests': undefined,
'X-clientType': this.#session.mfa.clientType,
'X-format': 'json',
'X-locale': this.#session.mfa.locale,
'X-login': this.#session.mfa.login,
'X-preAuthToken': this.#session.mfa.preAuthToken,
'X-version': '7.0',
'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'),
},
}));
if (sessions.axiosMethods.status >= 400) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `The remote server responded with a HTTP ${sessions.axiosMethods.status} status code`);
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `The remote server responded with a HTTP ${sessions.axiosMethods.status} status code`,
},
};
}
if (typeof sessions.axiosMethods?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosMethodsRequestPath = sessions.axiosMethods.request.path;
const axiosMethodsRequestPathValid = requestPathNgaServRunRraProxyHrefRestIcontrolUiClientMultiFactorAuthSatXx.test(axiosMethodsRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path ➜ ${axiosMethodsRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'info', `Request path valid ➜ ${axiosMethodsRequestPathValid}`);
}
if (!axiosMethodsRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', `"${axiosMethodsRequestPath}" is not the MFA auth page`);
}
this.handleLoginFailure(axiosMethodsRequestPath, sessions.axiosMethods);
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: `"${axiosMethodsRequestPath}" is not the MFA auth page`,
},
};
}
const parsedMethods = multiFactorAuth.safeParse(sessions.axiosMethods.data);
if (!parsedMethods.success) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'The response body of the MFA auth page is invalid');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'The response body of the MFA auth page is invalid',
},
};
}
const parsedMultiFactorMethods = parseMultiFactorMethods(parsedMethods.data);
if (parsedMultiFactorMethods.length === 0) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Failed to retrieve verification methods');
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
message: 'Failed to retrieve verification methods',
},
};
}
this.#session.mfa.verificationMethods = parsedMultiFactorMethods;
this.#session.status = 'complete';
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'success', `Successfully retrieved verification methods from "${this.#internal.baseUrl}"`);
}
return {
action: 'GET_VERIFICATION_METHODS',
success: true,
info: {
methods: parsedMultiFactorMethods,
status: this.#session.status,
},
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getVerificationMethods()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'GET_VERIFICATION_METHODS',
success: false,
info: {
error: errorObject,
},
};
}
async requestCode(methodId) {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Attempting to request a code using the "${methodId}" method at "${this.#internal.baseUrl}"`);
}
if (this.#session.mfa.verificationMethods.find((device) => device.id === methodId) === undefined) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `The "${methodId}" verification method does not exist. Did you run the "getVerificationMethods()" yet?`);
}
return {
action: 'REQUEST_CODE',
success: false,
info: {
message: `The "${methodId}" verification method does not exist. Did you run the "getVerificationMethods()" yet?`,
},
};
}
try {
const sessions = {};
const requestCodeForm = new URLSearchParams();
requestCodeForm.append('id', methodId);
sessions.axiosRequestCode = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/requestOtpForRegisteredProperty&sat=${this.#session.mfa.satCode}`, requestCodeForm, this.getRequestConfig({
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: this.#internal.baseUrl,
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': undefined,
'Upgrade-Insecure-Requests': undefined,
'X-clientType': this.#session.mfa.clientType,
'X-format': 'json',
'X-locale': this.#session.mfa.locale,
'X-login': this.#session.mfa.login,
'X-preAuthToken': this.#session.mfa.preAuthToken,
'X-version': '7.0',
'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'),
},
}));
if (typeof sessions.axiosRequestCode?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'REQUEST_CODE',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosRequestCodeRequestPath = sessions.axiosRequestCode.request.path;
const axiosRequestCodeRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthRequestOtpForRegisteredPropertySatXx.test(axiosRequestCodeRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Request path ➜ ${axiosRequestCodeRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'info', `Request path valid ➜ ${axiosRequestCodeRequestPathValid}`);
}
if (!axiosRequestCodeRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `"${axiosRequestCodeRequestPath}" is not the request code page`);
}
this.handleLoginFailure(axiosRequestCodeRequestPath, sessions.axiosRequestCode);
return {
action: 'REQUEST_CODE',
success: false,
info: {
message: `"${axiosRequestCodeRequestPath}" is not the request code page`,
},
};
}
const parsedRequestCode = otpResponse.safeParse(sessions.axiosRequestCode.data);
if (!parsedRequestCode.success) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'The response body of the request code page is invalid');
}
return {
action: 'REQUEST_CODE',
success: false,
info: {
message: 'The response body of the request code page is invalid',
},
};
}
if (!parsedRequestCode.data.detail.includes('OK')) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', `Unable to get a verification code ➜ "${parsedRequestCode.data.detail}"`);
}
return {
action: 'REQUEST_CODE',
success: false,
info: {
message: `Unable to get a verification code ➜ "${parsedRequestCode.data.detail}"`,
},
};
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'success', `Successfully requested a code for "${methodId}" at "${this.#internal.baseUrl}"`);
}
return {
action: 'REQUEST_CODE',
success: true,
info: null,
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.requestCode()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'REQUEST_CODE',
success: false,
info: {
error: errorObject,
},
};
}
async validateCode(otpCode) {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Attempting to validate verification code at "${this.#internal.baseUrl}"`);
}
if (!textOneTimePasscode.test(otpCode)) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', `"${otpCode}" is an invalid verification code format`);
}
return {
action: 'VALIDATE_CODE',
success: false,
info: {
message: `"${otpCode}" is an invalid verification code format`,
},
};
}
try {
const sessions = {};
const validateCodeForm = new URLSearchParams();
validateCodeForm.append('otp', otpCode);
sessions.axiosValidateCode = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/validateOtp&sat=${this.#session.mfa.satCode}`, validateCodeForm, this.getRequestConfig({
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: this.#internal.baseUrl,
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': undefined,
'Upgrade-Insecure-Requests': undefined,
'X-clientType': this.#session.mfa.clientType,
'X-format': 'json',
'X-locale': this.#session.mfa.locale,
'X-login': this.#session.mfa.login,
'X-preAuthToken': this.#session.mfa.preAuthToken,
'X-version': '7.0',
'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'),
},
}));
if (typeof sessions.axiosValidateCode?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'VALIDATE_CODE',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosValidateCodeRequestPath = sessions.axiosValidateCode.request.path;
const axiosValidateCodeRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthValidateOtpSatXx.test(axiosValidateCodeRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Request path ➜ ${axiosValidateCodeRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'info', `Request path valid ➜ ${axiosValidateCodeRequestPathValid}`);
}
if (!axiosValidateCodeRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', `"${axiosValidateCodeRequestPath}" is not the validate code page`);
}
this.handleLoginFailure(axiosValidateCodeRequestPath, sessions.axiosValidateCode);
return {
action: 'VALIDATE_CODE',
success: false,
info: {
message: `"${axiosValidateCodeRequestPath}" is not the validate code page`,
},
};
}
if (typeof sessions.axiosValidateCode.data === 'string'
&& sessions.axiosValidateCode.data === '') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'Already passed verification');
}
return {
action: 'VALIDATE_CODE',
success: true,
info: null,
};
}
const parsedValidateCode = otpResponse.safeParse(sessions.axiosValidateCode.data);
if (!parsedValidateCode.success) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The response body of the validate code page is invalid');
}
return {
action: 'VALIDATE_CODE',
success: false,
info: {
message: 'The response body of the validate code page is invalid',
},
};
}
if (!parsedValidateCode.data.detail.startsWith('u=')) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'The verification code submitted is either invalidated or expired');
}
return {
action: 'VALIDATE_CODE',
success: false,
info: {
message: 'The verification code submitted is either invalidated or expired',
},
};
}
this.#session.mfa.token = parsedValidateCode.data.detail;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'success', `Successfully validated verification code at "${this.#internal.baseUrl}"`);
}
return {
action: 'VALIDATE_CODE',
success: true,
info: null,
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.validateCode()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'VALIDATE_CODE',
success: false,
info: {
error: errorObject,
},
};
}
async getTrustedDevices() {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Attempting to retrieve trusted devices at "${this.#internal.baseUrl}"`);
}
try {
const sessions = {};
sessions.axiosDevicePoll = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?only=client.multiFactorAuth&exclude=&sat=${this.#session.mfa.satCode}&href=rest/adt/ui/updates&sat=${this.#session.mfa.satCode}&`, this.getRequestConfig({
headers: {
Accept: 'application/json',
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': undefined,
'Upgrade-Insecure-Requests': undefined,
'X-clientType': this.#session.mfa.clientType,
'X-format': 'json',
'X-locale': this.#session.mfa.locale,
'X-login': this.#session.mfa.login,
'X-token': this.#session.mfa.token,
'X-version': '7.0',
'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'),
},
}));
if (typeof sessions.axiosDevicePoll?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'GET_TRUSTED_DEVICES',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosDevicePollRequestPath = sessions.axiosDevicePoll.request.path;
const axiosDevicePollRequestPathValid = requestPathNgaServRunRraProxyOnlyClientMultiFactorAuthExcludeSatXxHrefRestAdtUiUpdatesSatXx.test(axiosDevicePollRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Request path ➜ ${axiosDevicePollRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'info', `Request path valid ➜ ${axiosDevicePollRequestPathValid}`);
}
if (!axiosDevicePollRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', `"${axiosDevicePollRequestPath}" is not the device polling page`);
}
this.handleLoginFailure(axiosDevicePollRequestPath, sessions.axiosDevicePoll);
return {
action: 'GET_TRUSTED_DEVICES',
success: false,
info: {
message: `"${axiosDevicePollRequestPath}" is not the device polling page`,
},
};
}
const parsedDevicePoll = multiFactorAuth.safeParse(_.get(sessions.axiosDevicePoll.data, ['update', 0, 'data', 'client', 'multiFactorAuth']));
if (!parsedDevicePoll.success) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'The response body of the device polling page is invalid');
}
return {
action: 'GET_TRUSTED_DEVICES',
success: false,
info: {
message: 'The response body of the device polling page is invalid',
},
};
}
const parsedTrustedDevices = parseMultiFactorTrustedDevices(parsedDevicePoll.data);
this.#session.mfa.trustedDevices = parsedTrustedDevices;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'success', `Successfully retrieved trusted devices at "${this.#internal.baseUrl}"`);
}
return {
action: 'GET_TRUSTED_DEVICES',
success: true,
info: {
trustedDevices: parsedTrustedDevices,
},
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getTrustedDevices()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'GET_TRUSTED_DEVICES',
success: false,
info: {
error: errorObject,
},
};
}
async addTrustedDevice(deviceName) {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Attempting to add trusted device at "${this.#internal.baseUrl}"`);
}
if (this.#session.mfa.trustedDevices.find((trustedDevice) => trustedDevice.name === encodeURIComponent(deviceName)) !== undefined) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The name for the trusted device already exists');
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: 'The name for the trusted device already exists',
},
};
}
if (deviceName.length < 1 || deviceName.length > 100) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The name for the trusted device must be between 1 to 100 characters');
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: 'The name for the trusted device must be between 1 to 100 characters',
},
};
}
try {
const sessions = {};
const trustedDeviceForm = new URLSearchParams();
trustedDeviceForm.append('name', encodeURIComponent(deviceName));
sessions.axiosAddDevice = await this.#session.httpClient.post(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/nga/serv/RunRRAProxy?href=rest/adt/ui/client/multiFactorAuth/addTrustedDevice&sat=${this.#session.mfa.satCode}`, trustedDeviceForm, this.getRequestConfig({
headers: {
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
Origin: this.#internal.baseUrl,
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-User': undefined,
'Upgrade-Insecure-Requests': undefined,
'X-clientType': this.#session.mfa.clientType,
'X-format': 'json',
'X-locale': this.#session.mfa.locale,
'X-login': this.#session.mfa.login,
'X-token': this.#session.mfa.token,
'X-version': '7.0',
'x-dtpc': generateFakeDynatracePCHeaderValue('multi-factor'),
},
}));
if (typeof sessions.axiosAddDevice?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosAddDeviceRequestPath = sessions.axiosAddDevice.request.path;
const axiosAddDeviceRequestPathValid = requestPathNgaServRunRraProxyHrefRestAdtUiClientMultiFactorAuthAddTrustedDeviceSatXx.test(axiosAddDeviceRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Request path ➜ ${axiosAddDeviceRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'info', `Request path valid ➜ ${axiosAddDeviceRequestPathValid}`);
}
if (!axiosAddDeviceRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', `"${axiosAddDeviceRequestPath}" is not the add trusted device page`);
}
this.handleLoginFailure(axiosAddDeviceRequestPath, sessions.axiosAddDevice);
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: `"${axiosAddDeviceRequestPath}" is not the add trusted device page`,
},
};
}
const parsedValidateCode = otpResponse.safeParse(sessions.axiosAddDevice.data);
if (!parsedValidateCode.success) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'The response body of the add trusted device page is invalid');
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: 'The response body of the add trusted device page is invalid',
},
};
}
if (!parsedValidateCode.data.detail.includes('OK')) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', `The trusted device could not be added ➜ "${parsedValidateCode.data.detail}"`);
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
message: `The trusted device could not be added ➜ "${parsedValidateCode.data.detail}"`,
},
};
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'success', `Successfully added trusted device at "${this.#internal.baseUrl}"`);
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: true,
info: null,
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.addTrustedDevice()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'ADD_TRUSTED_DEVICE',
success: false,
info: {
error: errorObject,
},
};
}
async completeSignIn() {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Attempting to complete sign in at "${this.#internal.baseUrl}"`);
}
try {
const sessions = {};
sessions.axiosPostSignIn = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/access/PostSigninProcessServ`, this.getRequestConfig({
headers: {
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/mfa/mfaSignIn.jsp?workflow=challenge`,
'Sec-Fetch-Site': 'same-origin',
},
}));
if (sessions.axiosPostSignIn.status >= 400) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', `The remote server responded with a HTTP ${sessions.axiosPostSignIn.status} status code`);
}
return {
action: 'COMPLETE_SIGN_IN',
success: false,
info: {
message: `The remote server responded with a HTTP ${sessions.axiosPostSignIn.status} status code`,
},
};
}
if (typeof sessions.axiosPostSignIn?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'COMPLETE_SIGN_IN',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosPostSignInRequestPath = sessions.axiosPostSignIn.request.path;
const axiosPostSignInRequestPathValid = requestPathSummarySummary.test(axiosPostSignInRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Request path ➜ ${axiosPostSignInRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'info', `Request path valid ➜ ${axiosPostSignInRequestPathValid}`);
}
if (!axiosPostSignInRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', `"${axiosPostSignInRequestPath}" is not the summary page`);
}
this.handleLoginFailure(axiosPostSignInRequestPath, sessions.axiosPostSignIn);
return {
action: 'COMPLETE_SIGN_IN',
success: false,
info: {
message: `"${axiosPostSignInRequestPath}" is not the summary page`,
},
};
}
this.#session.mfa = {
clientType: null,
locale: null,
login: null,
preAuthToken: null,
satCode: null,
token: null,
trustedDevices: [],
verificationMethods: [],
};
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'success', `Successfully completed sign in at "${this.#internal.baseUrl}"`);
}
return {
action: 'COMPLETE_SIGN_IN',
success: true,
info: null,
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.completeSignIn()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'COMPLETE_SIGN_IN',
success: false,
info: {
error: errorObject,
},
};
}
async getSensors() {
let errorObject;
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'info', `Attempting to retrieve sensors from "${this.#internal.baseUrl}"`);
}
try {
const sessions = {};
sessions.axiosSystem = await this.#session.httpClient.get(`${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/system/system.jsp`, this.getRequestConfig({
headers: {
Referer: `${this.#internal.baseUrl}/myhome/${this.#session.portalVersion}/summary/summary.jsp`,
'Sec-Fetch-Site': 'same-origin',
},
}));
if (sessions.axiosSystem.status >= 400) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'error', `The remote server responded with a HTTP ${sessions.axiosSystem.status} status code`);
}
return {
action: 'GET_SENSORS',
success: false,
info: {
message: `The remote server responded with a HTTP ${sessions.axiosSystem.status} status code`,
},
};
}
if (typeof sessions.axiosSystem?.request === 'undefined') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'error', 'The HTTP client responded without the "request" object');
}
return {
action: 'GET_SENSORS',
success: false,
info: {
message: 'The HTTP client responded without the "request" object',
},
};
}
const axiosSystemRequestPath = sessions.axiosSystem.request.path;
const axiosSystemRequestPathValid = requestPathSystemSystem.test(axiosSystemRequestPath);
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'info', `Request path ➜ ${axiosSystemRequestPath}`);
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'info', `Request path valid ➜ ${axiosSystemRequestPathValid}`);
}
if (!axiosSystemRequestPathValid) {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'error', `"${axiosSystemRequestPath}" is not the system page`);
}
this.handleLoginFailure(axiosSystemRequestPath, sessions.axiosSystem);
return {
action: 'GET_SENSORS',
success: false,
info: {
message: `"${axiosSystemRequestPath}" is not the system page`,
},
};
}
if (typeof sessions.axiosSystem.data !== 'string') {
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'error', 'The response body of the system page is not of type "string"');
}
return {
action: 'GET_SENSORS',
success: false,
info: {
message: 'The response body of the system page is not of type "string"',
},
};
}
sessions.jsdomSystem = new JSDOM(sessions.axiosSystem.data, {
url: sessions.axiosSystem.config.url,
referrer: sessions.axiosSystem.config.headers.Referer,
contentType: 'text/html',
pretendToBeVisual: true,
});
const jsdomSystemSensorsTable = sessions.jsdomSystem.window.document.querySelectorAll('#systemContentList tr[class^=\'p_row\'] tr.p_listRow');
const parsedSensorsConfigTable = parseSensorsTable('sensors-config', jsdomSystemSensorsTable);
await this.newInformationDispatcher('debug-parser', {
method: 'generateSensorsConfig',
response: parsedSensorsConfigTable,
rawHtml: sessions.axiosSystem.data,
});
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'success', `Successfully retrieved sensors from "${this.#internal.baseUrl}"`);
}
return {
action: 'GET_SENSORS',
success: true,
info: {
sensors: parsedSensorsConfigTable,
},
};
}
catch (error) {
errorObject = (isErrorLike(error)) ? serializeError(error) : serializeError(new Error('Unknown error'));
}
if (this.#internal.debug) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.getSensors()', 'error', 'Method encountered an error during execution');
stackTracer('serialize-error', errorObject);
}
return {
action: 'GET_SENSORS',
success: false,
info: {
error: errorObject,
},
};
}
getFingerprint() {
return this.#credentials.fingerprint;
}
resetSession() {
this.#session = {
httpClient: wrapper(axios.create({
jar: new CookieJar(),
validateStatus: () => true,
})),
mfa: {
clientType: null,
locale: null,
login: null,
preAuthToken: null,
satCode: null,
token: null,
trustedDevices: [],
verificationMethods: [],
},
portalVersion: null,
status: 'logged-out',
};
}
async newInformationDispatcher(type, data) {
const dataHash = generateHash(data);
if (this.#internal.reportedHashes.find((reportedHash) => dataHash === reportedHash) === undefined) {
let detectedNew = false;
switch (type) {
case 'debug-parser':
detectedNew = await detectGlobalDebugParser(data, null, this.#internal.debug);
break;
case 'portal-version':
detectedNew = await detectGlobalPortalVersion(data, null, this.#internal.debug);
break;
default:
break;
}
if (detectedNew) {
this.#internal.reportedHashes.push(dataHash);
}
}
}
getRequestConfig(extraConfig) {
const defaultConfig = {
family: 4,
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
Host: `${this.#connection.subdomain}.adtpulse.com`,
Pragma: 'no-cache',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
},
timeout: 15000,
};
if (extraConfig === undefined) {
return defaultConfig;
}
return _.merge(_.omit(defaultConfig, findNullKeys(extraConfig)), _.omit(extraConfig, findNullKeys(extraConfig)));
}
handleLoginFailure(requestPath, session) {
if (requestPath === null) {
return;
}
if (requestPathAccessSignIn.test(requestPath) || requestPathAccessSignInEXxPartnerAdt.test(requestPath)) {
if (this.#internal.debug) {
const errorMessage = fetchErrorMessage(session);
if (requestPathAccessSignIn.test(requestPath) || requestPathAccessSignInEXxPartnerAdt.test(requestPath)) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.handleLoginFailure()', 'error', 'Either the username or password is incorrect, fingerprint format is invalid, or was signed out due to inactivity');
}
if (requestPathSummarySummary.test(requestPath)) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.handleLoginFailure()', 'error', 'The generated fingerprint is already validated');
}
if (errorMessage !== null) {
debugLog(this.#internal.logger, 'auth.ts / ADTPulseAuth.handleLoginFailure()', 'warn', `Portal message ➜ "${errorMessage}"`);
}
}
this.resetSession();
}
}
}
//# sourceMappingURL=auth.js.map