import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
// Add stealth plugin to avoid detection
(puppeteer as any).use(StealthPlugin());
import { AuthConfig, SessionInfo } from './types.js';
import { ErrorHandler } from './ErrorHandler.js';
export class SuperstoreAuth {
private baseUrl = 'https://www.realcanadiansuperstore.ca';
private loginUrl = `${this.baseUrl}/login`;
private accountUrl = `${this.baseUrl}/account`;
private browser: any = null;
private page: any = null;
private sessionInfo: SessionInfo = { isAuthenticated: false };
private sessionTimeout: number;
private retryAttempts = 3;
private retryDelay = 2000;
private authToken: string | null = null;
constructor(config?: AuthConfig) {
this.sessionTimeout = config?.sessionTimeout || 3600000; // 1 hour default
}
/**
* Initialize browser instance with optimized settings
*/
private async initializeBrowser(): Promise<void> {
if (this.browser) return;
this.browser = await (puppeteer as any).launch({
headless: true, // Use boolean true instead of 'new'
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-blink-features=AutomationControlled', // Hide automation
'--disable-web-security',
'--disable-features=VizDisplayCompositor'
],
defaultViewport: { width: 1366, height: 768 },
timeout: 30000 // 30 second timeout
});
this.page = await this.browser.newPage();
// Set realistic user agent and headers
await this.page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
// Set additional headers to mimic real browser
await this.page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
}
/**
* Login to Superstore with credentials from environment variables
*/
async login(): Promise<boolean> {
return await ErrorHandler.handleAuthError(async () => {
const username = process.env.SUPERSTORE_EMAIL;
const password = process.env.SUPERSTORE_PASSWORD;
if (!username || !password) {
throw new Error('SUPERSTORE_EMAIL and SUPERSTORE_PASSWORD environment variables are required');
}
await this.initializeBrowser();
if (!this.page) throw new Error('Failed to initialize browser page');
// Navigate to login page
await this.page.goto(this.loginUrl, { waitUntil: 'networkidle2', timeout: 30000 });
// Wait for page to fully load
await new Promise(resolve => setTimeout(resolve, 3000));
// Wait for login form to be visible (try multiple selectors)
try {
await this.page.waitForSelector('input[type="email"], input[name="email"], input[id="email"], input[type="text"]', { timeout: 10000 });
} catch (e) {
// If email field not found, might already be on a different page
console.log('Email input not found immediately, checking page state...');
}
// Find email input field (try multiple selectors)
const emailSelectors = [
'input[type="email"]',
'input[name="email"]',
'input[id="email"]',
'input[placeholder*="email" i]',
'input[placeholder*="username" i]'
];
let emailField = null;
for (const selector of emailSelectors) {
try {
emailField = await this.page.$(selector);
if (emailField) break;
} catch (e) {
continue;
}
}
if (!emailField) {
throw new Error('Could not find email input field');
}
// Find password input field
const passwordSelectors = [
'input[type="password"]',
'input[name="password"]',
'input[id="password"]'
];
let passwordField = null;
for (const selector of passwordSelectors) {
try {
passwordField = await this.page.$(selector);
if (passwordField) break;
} catch (e) {
continue;
}
}
if (!passwordField) {
throw new Error('Could not find password input field');
}
// Clear and fill email field
await emailField.click({ clickCount: 3 });
await emailField.type(username, { delay: 100 });
// Clear and fill password field
await passwordField.click({ clickCount: 3 });
await passwordField.type(password, { delay: 100 });
// Find and click login button
const loginButtonSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:contains("Sign In")',
'button:contains("Login")',
'button:contains("Log In")',
'[data-testid*="login"]',
'[data-testid*="signin"]'
];
let loginButton = null;
for (const selector of loginButtonSelectors) {
try {
loginButton = await this.page.$(selector);
if (loginButton) break;
} catch (e) {
continue;
}
}
if (!loginButton) {
// Try to find any button that might be the login button
const buttons = await this.page.$$('button');
for (const button of buttons) {
const text = await button.evaluate((el: any) => el.textContent?.toLowerCase() || '');
if (text.includes('sign in') || text.includes('login') || text.includes('log in')) {
loginButton = button;
break;
}
}
}
if (!loginButton) {
throw new Error('Could not find login button');
}
// Click login button and wait for navigation
await Promise.all([
this.page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 }),
loginButton.click()
]);
// After PCID login, navigate to account page to trigger token exchange
console.log('Login redirect complete, navigating to account page...');
await this.page.goto(this.accountUrl, { waitUntil: 'networkidle2', timeout: 30000 });
// Wait for AccessToken cookie to be set (poll for up to 15 seconds)
console.log('Waiting for AccessToken cookie to be set...');
let accessTokenCookie = null;
const maxWaitTime = 15000; // 15 seconds
const pollInterval = 500; // Check every 500ms
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
const cookies = await this.page.cookies();
accessTokenCookie = cookies.find((c: any) => c.name === 'AccessToken');
if (accessTokenCookie && accessTokenCookie.value && accessTokenCookie.value.startsWith('eyJ')) {
console.log('AccessToken cookie found!');
break;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
if (accessTokenCookie && accessTokenCookie.value && accessTokenCookie.value.startsWith('eyJ')) {
this.authToken = accessTokenCookie.value;
console.log('Successfully captured authentication token from AccessToken cookie');
console.log('Token preview:', this.authToken?.substring(0, 50) + '...');
this.sessionInfo = {
isAuthenticated: true,
sessionExpiry: new Date(Date.now() + this.sessionTimeout),
lastActivity: new Date()
};
return true;
} else {
// Check for error messages
const errorSelectors = [
'[class*="error"]',
'[class*="alert"]',
'[class*="message"]',
'.error',
'.alert',
'.message'
];
for (const selector of errorSelectors) {
const errorElement = await this.page.$(selector);
if (errorElement) {
const errorText = await errorElement.evaluate((el: any) => el.textContent);
if (errorText && errorText.toLowerCase().includes('invalid') ||
errorText.toLowerCase().includes('incorrect') ||
errorText.toLowerCase().includes('error')) {
throw new Error(`Login failed: ${errorText}`);
}
}
}
throw new Error('Login failed: Unable to verify successful authentication');
}
}, 'login');
}
/**
* Check if currently authenticated
*/
async isAuthenticated(): Promise<boolean> {
if (!this.sessionInfo.isAuthenticated) return false;
// Check if session has expired
if (this.sessionInfo.sessionExpiry && new Date() > this.sessionInfo.sessionExpiry) {
this.sessionInfo = { isAuthenticated: false };
return false;
}
// Verify session is still valid by checking account page
try {
if (!this.page) {
await this.initializeBrowser();
}
if (!this.page) return false;
await this.page.goto(this.accountUrl, { waitUntil: 'networkidle2', timeout: 10000 });
// Check if we're still logged in by looking for account-specific elements
const isLoggedIn = await this.page.$('[data-testid*="account"]') ||
await this.page.$('[class*="account"]') ||
await this.page.$('[class*="profile"]') ||
this.page.url().includes('/account');
if (!isLoggedIn) {
this.sessionInfo = { isAuthenticated: false };
return false;
}
this.sessionInfo.lastActivity = new Date();
return true;
} catch (error) {
console.error('Authentication check failed:', error);
this.sessionInfo = { isAuthenticated: false };
return false;
}
}
/**
* Refresh session by re-authenticating
*/
async refreshSession(): Promise<boolean> {
this.sessionInfo = { isAuthenticated: false };
return await this.login();
}
/**
* Get current session cookies
*/
async getCookies(): Promise<any[]> {
if (!this.page) return [];
return await this.page.cookies();
}
/**
* Get cookies as a tough-cookie jar-compatible structure
*/
async getCookieJarLike(): Promise<{ name: string; value: string; domain?: string; path?: string; expires?: number; secure?: boolean; httpOnly?: boolean; sameSite?: string; }[]> {
const cookies = await this.getCookies();
return cookies.map((c: any) => ({
name: c.name,
value: c.value,
domain: c.domain?.replace(/^\./, ''),
path: c.path,
expires: typeof c.expires === 'number' ? c.expires : undefined,
secure: c.secure,
httpOnly: c.httpOnly,
sameSite: c.sameSite
}));
}
/**
* Get the auth token (JWT bearer token)
*/
getAuthToken(): string | null {
return this.authToken;
}
/**
* Make authenticated request with cookie injection
*/
async fetchWithAuth(url: string, options: RequestInit = {}): Promise<Response> {
if (!await this.isAuthenticated()) {
await this.refreshSession();
}
if (!this.page) {
throw new Error('No active session available');
}
const cookies = await this.getCookies();
const cookieString = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Cookie': cookieString,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response;
}
/**
* Logout and clear session
*/
async logout(): Promise<void> {
try {
if (this.page) {
// Try to find and click logout button
const logoutSelectors = [
'a[href*="logout"]',
'button:contains("Logout")',
'button:contains("Sign Out")',
'[data-testid*="logout"]'
];
for (const selector of logoutSelectors) {
try {
const logoutButton = await this.page.$(selector);
if (logoutButton) {
await logoutButton.click();
await this.page.waitForTimeout(2000);
break;
}
} catch (e) {
continue;
}
}
}
} catch (error) {
console.error('Logout error:', error);
} finally {
this.sessionInfo = { isAuthenticated: false };
if (this.page) {
await this.page.close();
this.page = null;
}
if (this.browser) {
await this.browser.close();
this.browser = null;
}
}
}
/**
* Get session information
*/
getSessionInfo(): SessionInfo {
return { ...this.sessionInfo };
}
/**
* Cleanup resources
*/
async cleanup(): Promise<void> {
await this.logout();
}
}