homebridge-mopar
Version:
Homebridge plugin for Mopar vehicles (Chrysler, Dodge, Jeep, Ram, Fiat, Alfa Romeo) with Uconnect
897 lines (779 loc) • 35.5 kB
JavaScript
/**
* Automated Authentication Module
*
* Handles automatic login to Mopar.com using Puppeteer
* to bypass Gigya's bot detection and cookie expiration issues.
*/
const puppeteer = require('puppeteer');
class MoparAuth {
constructor(email, password, log = console.log, debugMode = false) {
this.email = email;
this.password = password;
this.log = log;
this.debugMode = debugMode;
this.cookies = null;
this.lastLogin = null;
}
// Debug logging helper
debug(message) {
if (this.debugMode) {
this.log(`[DEBUG] ${message}`);
}
}
/**
* Login to Mopar.com and extract authentication cookies
* @returns {Object} Cookie object with all necessary cookies
*/
async login() {
this.log('Starting automated login...');
// Try to find Chrome in common locations
const fs = require('fs');
const os = require('os');
const path = require('path');
const launchOptions = {
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
],
};
const chromePaths = [
// Puppeteer cache for various users
path.join(os.homedir(), '.cache/puppeteer/chrome/linux-141.0.7390.54/chrome-linux64/chrome'),
'/root/.cache/puppeteer/chrome/linux-141.0.7390.54/chrome-linux64/chrome',
'/var/lib/homebridge/.cache/puppeteer/chrome/linux-141.0.7390.54/chrome-linux64/chrome',
'/home/homebridge/.cache/puppeteer/chrome/linux-141.0.7390.54/chrome-linux64/chrome',
// System installations
'/usr/bin/chromium-browser',
'/usr/bin/chromium',
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/snap/bin/chromium',
'/opt/google/chrome/chrome',
];
for (const chromePath of chromePaths) {
if (fs.existsSync(chromePath)) {
launchOptions.executablePath = chromePath;
this.log(`Using Chrome at: ${chromePath}`);
break;
}
}
const browser = await puppeteer.launch(launchOptions);
try {
const page = await browser.newPage();
// Set realistic viewport and user agent
await page.setViewport({ width: 1920, height: 1080 });
await page.setUserAgent(
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
this.log(' Navigating to login page...');
await page.goto('https://www.mopar.com/en-us/sign-in.html', {
waitUntil: 'networkidle2',
timeout: 60000, // Increased for slower devices
});
// Wait for Gigya login form to load
this.log(' Waiting for login form...');
await page.waitForSelector('input[name="username"]', { timeout: 20000 }); // Increased timeout
await page.waitForSelector('input[name="password"]', { timeout: 20000 });
// Wait for form to be fully ready
await new Promise((resolve) => setTimeout(resolve, 2000));
// Fill in credentials - with verification
this.debug('Entering credentials...');
// Method 1: Try clicking and typing (most realistic)
let emailEntered = false;
try {
await page.click('input[name="username"]');
await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');
await page.type('input[name="username"]', this.email, { delay: 50 });
emailEntered = true;
} catch (error) {
this.debug(`Click/type method failed: ${error.message}`);
}
// Method 2: Direct JavaScript injection if Method 1 failed
if (!emailEntered) {
this.debug('Using JavaScript to fill email field...');
await page.evaluate((email) => {
const field = document.querySelector('input[name="username"]');
field.value = email;
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
}, this.email);
}
// Verify email was entered
const emailValue = await page.$eval('input[name="username"]', (el) => el.value);
this.debug(`Email field value: "${emailValue}"`);
if (!emailValue || emailValue.length === 0) {
throw new Error('Failed to enter email address');
}
// Fill password field
let passwordEntered = false;
try {
await page.click('input[name="password"]');
await page.keyboard.down('Control');
await page.keyboard.press('A');
await page.keyboard.up('Control');
await page.type('input[name="password"]', this.password, { delay: 50 });
passwordEntered = true;
} catch (error) {
this.debug(`Click/type method failed for password: ${error.message}`);
}
// Method 2: Direct JavaScript injection if Method 1 failed
if (!passwordEntered) {
this.debug('Using JavaScript to fill password field...');
await page.evaluate((password) => {
const field = document.querySelector('input[name="password"]');
field.value = password;
field.dispatchEvent(new Event('input', { bubbles: true }));
field.dispatchEvent(new Event('change', { bubbles: true }));
}, this.password);
}
// Verify password was entered (check length, not actual value)
const passwordValue = await page.$eval('input[name="password"]', (el) => el.value);
this.debug(`Password field length: ${passwordValue.length}`);
if (!passwordValue || passwordValue.length === 0) {
throw new Error('Failed to enter password');
}
// Trigger form validation events that Gigya expects
this.debug('Triggering form validation...');
await page.evaluate(() => {
const usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]');
// Fire all events that Gigya might be listening for
const events = ['input', 'change', 'blur', 'keyup'];
events.forEach((eventType) => {
if (usernameField) {
usernameField.dispatchEvent(new Event(eventType, { bubbles: true, cancelable: true }));
}
if (passwordField) {
passwordField.dispatchEvent(new Event(eventType, { bubbles: true, cancelable: true }));
}
});
});
// Wait for validation to complete
await new Promise((resolve) => setTimeout(resolve, 500));
// Take screenshot before submitting to verify credentials are filled
const beforeSubmitPath = '/tmp/mopar-before-submit.png';
await page.screenshot({ path: beforeSubmitPath, fullPage: true });
this.debug(`Screenshot before submit saved to: ${beforeSubmitPath}`);
// Submit the form - try pressing Enter first (most reliable)
this.log(' Submitting login...');
// Wait a moment for any JavaScript to finish loading
await new Promise((resolve) => setTimeout(resolve, 500));
// Enable request and response monitoring
let formSubmitted = false;
const submissionListener = (request) => {
const url = request.url();
const method = request.method();
if (method === 'POST' && (url.includes('accounts.login') || url.includes('signin') || url.includes('login'))) {
formSubmitted = true;
this.debug(`Form POST detected: ${url}`);
}
};
const responseListener = async (response) => {
const url = response.url();
if (url.includes('accounts.login') || url.includes('socialize.login')) {
try {
const text = await response.text();
this.debug(`Login API response received (${response.status()}): ${text.substring(0, 200)}...`);
// Try to parse as JSON and extract error info
try {
const json = JSON.parse(text);
if (json.errorCode) {
this.log(`Gigya Error Code: ${json.errorCode}`);
this.log(`Gigya Error Message: ${json.errorMessage || json.errorDetails || 'Unknown error'}`);
}
if (json.statusCode) {
this.debug(`Gigya Status Code: ${json.statusCode}`);
}
} catch (e) {
// Not JSON, that's okay
}
} catch (e) {
this.debug(`Could not read response body: ${e.message}`);
}
}
};
page.on('request', submissionListener);
page.on('response', responseListener);
// Method 1: Press Enter in password field (most reliable)
let submitMethod = 'none';
try {
await page.focus('input[name="password"]');
await page.keyboard.press('Enter');
submitMethod = 'enter-key';
this.debug('Pressed Enter to submit form');
} catch (error) {
this.debug(`Enter press failed: ${error.message}, trying click method...`);
}
// Wait a bit to see if Enter worked
await new Promise((resolve) => setTimeout(resolve, 1000));
// Try multiple selectors and click strategies if Enter didn't work
if (submitMethod === 'none' || !formSubmitted) {
const selectors = [
'input[type="submit"][value="Sign In"]',
'input[type="submit"]',
'button[type="submit"]',
'.gigya-input-submit',
'input.gigya-input-submit',
];
let clicked = false;
for (const selector of selectors) {
try {
const button = await page.$(selector);
if (button) {
// Check if button is visible and enabled
const buttonInfo = await page.evaluate((el) => {
const style = window.getComputedStyle(el);
return {
visible: style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0',
disabled: el.disabled,
text: el.value || el.textContent,
};
}, button);
this.debug(
`Found submit button (${selector}): visible=${buttonInfo.visible}, disabled=${buttonInfo.disabled}, text="${buttonInfo.text}"`
);
if (buttonInfo.visible && !buttonInfo.disabled) {
// Use JS click directly - regular click never works with Gigya forms
await page.evaluate((el) => el.click(), button);
clicked = true;
submitMethod = `js-click-${selector}`;
this.debug(`Clicked submit button using: ${selector}`);
break;
}
}
} catch (e) {
// Try next selector
continue;
}
}
if (!clicked && submitMethod === 'none') {
page.off('request', submissionListener);
throw new Error('Could not find or click login button');
}
}
// Wait a moment to see if form was submitted
await new Promise((resolve) => setTimeout(resolve, 500));
// If form still hasn't submitted, try direct form submission
if (!formSubmitted) {
this.debug('Form POST not detected, trying direct Gigya API submission...');
const formSubmitResult = await page.evaluate(() => {
return new Promise((resolve) => {
const form = document.querySelector('form');
if (form) {
// Try to find and call Gigya's submit handler with callbacks
if (typeof gigya !== 'undefined' && gigya.accounts && gigya.accounts.login) {
const usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]');
if (usernameField && passwordField) {
// Call Gigya's login API directly with event handlers
try {
// Set up event listeners for Gigya responses
window.gigyaLoginSuccess = false;
window.gigyaLoginError = null;
gigya.accounts.login({
loginID: usernameField.value,
password: passwordField.value,
callback: function (response) {
if (response.errorCode === 0) {
window.gigyaLoginSuccess = true;
resolve({ method: 'gigya-api', attempted: true, success: true });
} else {
window.gigyaLoginError = {
errorCode: response.errorCode,
errorMessage: response.errorMessage || response.errorDetails,
};
resolve({
method: 'gigya-api',
attempted: true,
success: false,
error: response.errorMessage || response.errorDetails,
});
}
},
});
// Timeout after 3 seconds if no callback
setTimeout(() => {
resolve({ method: 'gigya-api', attempted: true, timeout: true });
}, 3000);
return; // Wait for callback or timeout
} catch (e) {
resolve({ method: 'gigya-api', attempted: true, error: e.message });
return;
}
}
}
// Fallback: Try native form submission
try {
form.submit();
resolve({ method: 'form.submit()', attempted: true });
} catch (e) {
resolve({ method: 'form.submit()', attempted: true, error: e.message });
}
} else {
resolve({ attempted: false, error: 'No form found' });
}
});
});
this.debug(`Direct submission result: ${JSON.stringify(formSubmitResult)}`);
submitMethod = formSubmitResult.method || submitMethod;
// If we got an error from Gigya, report it immediately
if (formSubmitResult.error && formSubmitResult.success === false) {
page.off('request', submissionListener);
page.off('response', responseListener);
throw new Error(`Login failed: ${formSubmitResult.error}`);
}
// Wait longer for submission to process (Gigya can be slow)
await new Promise((resolve) => setTimeout(resolve, 3000));
}
page.off('request', submissionListener);
page.off('response', responseListener);
this.debug(`Submit method used: ${submitMethod}`);
this.debug(`Form POST detected: ${formSubmitted}`);
// After successful Gigya login, check if we're authenticated
this.debug('Checking for Gigya session...');
// Wait for Gigya to set session cookies and UID
await new Promise((resolve) => setTimeout(resolve, 2000));
const gigyaSession = await page.evaluate(() => {
if (typeof gigya !== 'undefined' && gigya.accounts && gigya.accounts.getAccountInfo) {
return new Promise((resolve) => {
gigya.accounts.getAccountInfo({
callback: function (response) {
if (response.errorCode === 0 && response.UID) {
resolve({
authenticated: true,
uid: response.UID,
profile: response.profile || {},
});
} else {
resolve({ authenticated: false, error: response.errorMessage });
}
},
});
// Timeout after 5 seconds
setTimeout(() => {
resolve({ authenticated: false, timeout: true });
}, 5000);
});
}
return { authenticated: false, noGigya: true };
});
this.debug(`Gigya session status: ${JSON.stringify(gigyaSession)}`);
// If authenticated with Gigya, POST the UID to establish Mopar session
if (gigyaSession.authenticated) {
this.log('Gigya session established, posting UID to establish Mopar session...');
try {
// Get UID signature and timestamp from Gigya
const gigyaData = await page.evaluate(() => {
return new Promise((resolve) => {
if (typeof gigya !== 'undefined' && gigya.accounts && gigya.accounts.getAccountInfo) {
gigya.accounts.getAccountInfo({
include: 'loginIDs',
callback: function (response) {
if (response.errorCode === 0) {
resolve({
uid: response.UID,
uidSignature: response.UIDSignature,
signatureTimestamp: response.signatureTimestamp,
});
} else {
resolve(null);
}
},
});
setTimeout(() => resolve(null), 5000);
} else {
resolve(null);
}
});
});
if (gigyaData && gigyaData.uid && gigyaData.uidSignature) {
this.debug('Got UID signature data');
// Get CSRF token from page
const csrfToken = await page.evaluate(() => {
const input = document.querySelector('input[name=":cq_csrf_token"]');
return input ? input.value : null;
});
if (csrfToken) {
this.debug(`Got CSRF token: ${csrfToken.substring(0, 20)}...`);
} else {
this.debug('Warning: No CSRF token found on current page');
}
// Build form data for POST to /sign-in
const formData = {
UID: gigyaData.uid,
UIDSignature: gigyaData.uidSignature,
signatureTimestamp: gigyaData.signatureTimestamp.toString(),
};
if (csrfToken) {
formData[':cq_csrf_token'] = csrfToken;
}
this.debug('Posting to /sign-in endpoint...');
// Use page.evaluate to submit via form to properly follow redirect
await page.evaluate((data) => {
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://www.mopar.com/sign-in';
for (const [key, value] of Object.entries(data)) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
}, formData);
// Wait for navigation after form submit (usually times out, we navigate manually)
try {
await page.waitForNavigation({
waitUntil: 'networkidle2',
timeout: 3000,
});
this.debug(`POST completed, navigated to: ${page.url()}`);
} catch (e) {
this.debug(`POST navigation timeout (expected): ${e.message}`);
}
// If we're not on a dashboard/vehicle page, try explicit navigation
const currentUrl = page.url();
if (!currentUrl.includes('my-vehicle') && !currentUrl.includes('dashboard')) {
this.debug('Not on owner site, navigating explicitly...');
await page.goto('https://www.mopar.com/chrysler/en-us/my-vehicle/dashboard.html', {
waitUntil: 'networkidle2',
timeout: 30000,
});
}
this.debug('Successfully navigated to owner site');
} else {
this.log(' Warning: Could not get UID signature, trying direct navigation...');
await page.goto('https://www.mopar.com/chrysler/en-us/my-vehicle/dashboard.html', {
waitUntil: 'networkidle2',
timeout: 30000,
});
}
} catch (e) {
this.log(` Navigation to owner site failed: ${e.message}`);
}
} else {
// Wait for automatic navigation
this.log(' Waiting for automatic login navigation...');
try {
await page.waitForNavigation({
waitUntil: 'networkidle2',
timeout: 15000,
});
} catch (e) {
this.log(' Navigation timeout, checking login status...');
}
}
// Extra time for cookies to settle and API to initialize
await new Promise((resolve) => setTimeout(resolve, 3000));
// Try to trigger the vehicle data load to ensure session is fully active
this.debug('Triggering vehicle data load...');
try {
await page.evaluate(() => {
// Try to trigger any pending requests by scrolling
window.scrollTo(0, 100);
});
await new Promise((resolve) => setTimeout(resolve, 2000));
} catch (e) {
this.log(` Could not trigger data load: ${e.message}`);
}
// Check if login was successful
const currentUrl = page.url();
this.log(` Current URL: ${currentUrl}`);
// Even if we're on sign-in page, check if we have a valid Gigya session
const isLoggedIn = !currentUrl.includes('sign-in') || gigyaSession.authenticated;
if (!isLoggedIn) {
// Still on sign-in page and no Gigya session, check for error messages
this.log(' Still on sign-in page without Gigya session, checking for errors...');
// Take a screenshot for debugging (saved to /tmp)
const screenshotPath = '/tmp/mopar-login-error.png';
try {
await page.screenshot({ path: screenshotPath, fullPage: true });
this.log(` Screenshot saved to: ${screenshotPath}`);
} catch (e) {
// Screenshot failed, not critical
}
// Capture page title for diagnostics
const pageTitle = await page.title();
this.log(` Page title: "${pageTitle}"`);
// Get all visible text from potential error containers
const diagnosticInfo = await page.evaluate(() => {
const info = {
allErrors: [],
visibleText: '',
formState: {},
captchaInfo: {},
};
// Check all possible error selectors
const errorSelectors = [
'.gigya-error-msg',
'.error-msg',
'.gigya-error-msg-active',
'[data-screenset-element-id*="error"]',
'.gigya-composite-control-error',
'.gigya-error',
'[class*="error"][style*="display: block"]',
'[class*="error"]:not([style*="display: none"])',
];
errorSelectors.forEach((selector) => {
const elements = document.querySelectorAll(selector);
elements.forEach((el) => {
const style = window.getComputedStyle(el);
if (style.display !== 'none' && style.visibility !== 'hidden' && el.textContent.trim()) {
info.allErrors.push({
selector: selector,
text: el.textContent.trim(),
html: el.innerHTML,
});
}
});
});
// Get the main content area text
const mainContent =
document.querySelector('.gigya-screen-content') || document.querySelector('form') || document.body;
if (mainContent) {
info.visibleText = mainContent.textContent.substring(0, 500);
}
// Check form state
const usernameField = document.querySelector('input[name="username"]');
const passwordField = document.querySelector('input[name="password"]');
const submitButton =
document.querySelector('input[type="submit"]') || document.querySelector('button[type="submit"]');
info.formState = {
usernamePresent: !!usernameField,
usernameValue: usernameField ? usernameField.value.substring(0, 5) + '...' : '',
passwordPresent: !!passwordField,
passwordLength: passwordField ? passwordField.value.length : 0,
submitButtonPresent: !!submitButton,
submitButtonDisabled: submitButton ? submitButton.disabled : null,
};
// Check for CAPTCHA elements with detailed info
// Only check for ACTUAL captcha iframes and reCAPTCHA elements
const captchaSelectors = [
'iframe[src*="recaptcha"]',
'iframe[src*="hcaptcha"]',
'iframe[title*="captcha"]',
'.g-recaptcha',
'#g-recaptcha',
'div[data-sitekey]', // reCAPTCHA div with data-sitekey
];
captchaSelectors.forEach((selector) => {
const elements = document.querySelectorAll(selector);
elements.forEach((el) => {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
const actuallyVisible =
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
rect.width > 0 &&
rect.height > 0;
if (actuallyVisible) {
info.captchaInfo[selector] = {
found: true,
visible: true,
display: style.display,
visibility: style.visibility,
opacity: style.opacity,
dimensions: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
innerHTML: el.innerHTML.substring(0, 200),
};
}
});
});
return info;
});
// Log all diagnostic information
this.log(' === DIAGNOSTIC INFORMATION ===');
this.log(` Errors found: ${diagnosticInfo.allErrors.length}`);
diagnosticInfo.allErrors.forEach((err, idx) => {
this.log(` Error ${idx + 1} (${err.selector}): ${err.text}`);
});
this.log(' Form state:');
this.log(` Username field present: ${diagnosticInfo.formState.usernamePresent}`);
this.log(` Username value: ${diagnosticInfo.formState.usernameValue}`);
this.log(` Password field present: ${diagnosticInfo.formState.passwordPresent}`);
this.log(` Password length: ${diagnosticInfo.formState.passwordLength}`);
this.log(` Submit button present: ${diagnosticInfo.formState.submitButtonPresent}`);
this.log(` Submit button disabled: ${diagnosticInfo.formState.submitButtonDisabled}`);
const captchaCount = Object.keys(diagnosticInfo.captchaInfo).length;
this.log(` CAPTCHA elements found: ${captchaCount}`);
Object.entries(diagnosticInfo.captchaInfo).forEach(([selector, info]) => {
this.log(` ${selector}: visible=${info.visible}, display=${info.display}, visibility=${info.visibility}`);
});
if (diagnosticInfo.visibleText) {
this.log(` Visible text sample: ${diagnosticInfo.visibleText.substring(0, 200)}...`);
}
this.log(' === END DIAGNOSTICS ===');
// Save HTML content for manual inspection
const htmlPath = '/tmp/mopar-login-error.html';
try {
const pageContent = await page.content();
const fs = require('fs');
fs.writeFileSync(htmlPath, pageContent);
this.log(` Page HTML saved to: ${htmlPath}`);
} catch (e) {
this.log(` Failed to save HTML: ${e.message}`);
}
// If we found an error message, report it
if (diagnosticInfo.allErrors.length > 0) {
const primaryError = diagnosticInfo.allErrors[0].text;
throw new Error(`Login failed: ${primaryError}`);
}
// Check if any CAPTCHA elements are actually visible
const visibleCaptchas = Object.keys(diagnosticInfo.captchaInfo);
if (visibleCaptchas.length > 0) {
this.log(' WARNING: Possible CAPTCHA elements detected, but they may be false positives');
Object.entries(diagnosticInfo.captchaInfo).forEach(([selector, info]) => {
this.log(` ${selector}: ${info.dimensions}`);
});
// Only throw if it's an actual reCAPTCHA iframe
const hasRealCaptcha = visibleCaptchas.some(
(s) => s.includes('iframe[src*="recaptcha"]') || s.includes('iframe[src*="hcaptcha"]')
);
if (hasRealCaptcha) {
throw new Error('Login blocked by CAPTCHA. Please try logging in manually through a browser first.');
}
}
// Check for verification requirement
const pageContent = await page.content();
if (pageContent.includes('verify your email') || pageContent.includes('Verify your email')) {
throw new Error('Account verification required. Please check your email or try logging in manually.');
}
// Generic failure message with helpful debugging info
throw new Error(
'Login failed: Still on sign-in page. Check credentials or review diagnostics above. Screenshots saved to /tmp/mopar-login-error.png and HTML to /tmp/mopar-login-error.html'
);
}
// If we have a Gigya session but still on sign-in URL, try once more to navigate
if (gigyaSession.authenticated && currentUrl.includes('sign-in')) {
this.log(' Have Gigya session but still on sign-in page, attempting final navigation...');
try {
await page.goto('https://www.mopar.com/en-us/my-vehicle.html', {
waitUntil: 'domcontentloaded',
timeout: 10000,
});
this.log(` Final navigation complete, now at: ${page.url()}`);
} catch (e) {
this.log(` Final navigation attempt failed, but continuing with Gigya session: ${e.message}`);
}
}
this.log('Login successful, extracting cookies...');
// Extract all cookies from all domains
const browserCookies = await page.cookies();
// Log all cookie domains for debugging
const domains = [...new Set(browserCookies.map((c) => c.domain))];
this.debug(`Cookie domains found: ${domains.join(', ')}`);
// Convert to simple object format
this.cookies = {};
const allowedDomains = ['mopar.com', 'gigya.com', 'stellantis.com'];
browserCookies.forEach((cookie) => {
// Save cookies from allowed domains
// Check if cookie domain ends with or equals any allowed domain
const isAllowed = allowedDomains.some(
(domain) => cookie.domain === domain || cookie.domain === `.${domain}` || cookie.domain.endsWith(`.${domain}`)
);
if (isAllowed) {
this.cookies[cookie.name] = cookie.value;
}
});
this.log(`Extracted ${Object.keys(this.cookies).length} cookies`);
// Log cookie names for debugging
const cookieNames = Object.keys(this.cookies).sort();
this.debug(`Cookie names: ${cookieNames.join(', ')}`);
// Check if we have the Gigya login token
const hasGltToken = cookieNames.some((name) => name.startsWith('glt_'));
if (!hasGltToken) {
this.log(' WARNING: No glt_ cookie found, attempting to extract from Gigya session...');
// Try to get login token from Gigya API
try {
const gigyaToken = await page.evaluate(() => {
if (typeof gigya !== 'undefined' && gigya.accounts && gigya.accounts.getAccountInfo) {
return new Promise((resolve) => {
gigya.accounts.getAccountInfo({
callback: function (response) {
if (response.errorCode === 0) {
// Look for login token in various places
const token = response.sessionInfo?.login_token || response.login_token || response.loginToken;
resolve(token || null);
} else {
resolve(null);
}
},
});
setTimeout(() => resolve(null), 5000);
});
}
return null;
});
if (gigyaToken) {
this.log(` Extracted login token from Gigya API: ${gigyaToken.substring(0, 20)}...`);
// Add it as a cookie in the expected format
this.cookies[`glt_${Date.now()}`] = gigyaToken;
} else {
this.log(' Could not extract login token from Gigya API');
}
} catch (e) {
this.log(` Failed to extract Gigya token: ${e.message}`);
}
}
this.lastLogin = new Date();
// Close browser without waiting (async cleanup)
// We have the cookies, no need to wait for graceful shutdown
browser.close().catch((e) => this.debug(`Browser close error (non-critical): ${e.message}`));
return this.cookies;
} catch (error) {
// User-friendly error messages
if (error.message.includes('net::ERR_NAME_NOT_RESOLVED') || error.code === 'ENOTFOUND') {
this.log.error('Cannot reach Mopar.com - Check your internet connection');
} else if (error.message.includes('timeout') || error.message.includes('Navigation timeout')) {
this.log.error('Login timed out - Mopar.com may be slow or unreachable');
this.log.error('Try again in a few minutes or check https://www.mopar.com/en-us/sign-in.html');
} else if (error.message.includes('ERR_CERT')) {
this.log.error('SSL/Certificate error - Check your system time and date settings');
} else if (error.message.includes('Execution context was destroyed')) {
this.log.error('Browser session crashed - This is usually temporary, try restarting Homebridge');
} else {
this.log.error(`Login failed: ${error.message}`);
this.log.error('Please verify your Mopar.com credentials are correct');
}
this.debug(`Full error: ${error.stack}`);
// Close browser on error
if (browser) {
browser.close().catch(() => {});
}
throw error;
}
}
/**
* Check if cookies are still valid (less than 20 hours old)
* @returns {boolean} True if cookies are valid
*/
areCookiesValid() {
if (!this.cookies || !this.lastLogin) {
return false;
}
const hoursSinceLogin = (Date.now() - this.lastLogin.getTime()) / (1000 * 60 * 60);
return hoursSinceLogin < 20; // Refresh before 24hr expiration
}
/**
* NOTE: Currently unused - login() is called directly instead
* Get cookies, refreshing if necessary
* @returns {Object} Valid cookie object
*/
async getCookies() {
if (!this.areCookiesValid()) {
this.log('Cookies expired or missing, logging in...');
await this.login();
}
return this.cookies;
}
}
module.exports = MoparAuth;