|
@@ -12,23 +12,33 @@ const ENV_PATH = path.join(DATA_HOME, 'booking.env');
|
|
|
const ADMIN_BASE = 'admin.booking.com';
|
|
const ADMIN_BASE = 'admin.booking.com';
|
|
|
const STRIP = new Set(['OptanonConsent', 'pcm_consent', 'uz']);
|
|
const STRIP = new Set(['OptanonConsent', 'pcm_consent', 'uz']);
|
|
|
|
|
|
|
|
-async function handleAuthAssurance(page: Page, masked: string): Promise<void> {
|
|
|
|
|
|
|
+async function handleAuthAssurance(page: Page, masked: string): Promise<boolean> {
|
|
|
console.error(`[booking-2fa] Challenge required (${masked || 'unknown number'}) — attempting SMS automation`);
|
|
console.error(`[booking-2fa] Challenge required (${masked || 'unknown number'}) — attempting SMS automation`);
|
|
|
|
|
|
|
|
|
|
+ // Verification options are <a> links, not buttons. Click the SMS link.
|
|
|
try {
|
|
try {
|
|
|
- await page.getByRole('button', { name: /sms|text message/i }).first().click({ timeout: 5_000 });
|
|
|
|
|
|
|
+ await page.getByText(/via Text message \(SMS\)/i).click({ timeout: 10_000 });
|
|
|
console.error('[booking-2fa] Clicked SMS option');
|
|
console.error('[booking-2fa] Clicked SMS option');
|
|
|
- await page.waitForTimeout(1_000);
|
|
|
|
|
|
|
+ await page.waitForLoadState('domcontentloaded');
|
|
|
} catch {
|
|
} catch {
|
|
|
- // Already on PIN entry, or SMS is the only option — continue.
|
|
|
|
|
|
|
+ // Already on PIN entry, or SMS nav already happened — continue.
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Select-phone page: click "Send verification code" to trigger the SMS.
|
|
|
|
|
+ try {
|
|
|
|
|
+ await page.getByRole('button', { name: /send verification code/i }).click({ timeout: 10_000 });
|
|
|
|
|
+ console.error('[booking-2fa] Clicked Send verification code');
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // Already past this step.
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const PIN_SEL = 'input[autocomplete="one-time-code"], input[inputmode="numeric"][maxlength], input[name*="pin"], input[name*="code"]';
|
|
const PIN_SEL = 'input[autocomplete="one-time-code"], input[inputmode="numeric"][maxlength], input[name*="pin"], input[name*="code"]';
|
|
|
let pinInput;
|
|
let pinInput;
|
|
|
try {
|
|
try {
|
|
|
- pinInput = await page.waitForSelector(PIN_SEL, { timeout: 15_000 });
|
|
|
|
|
|
|
+ pinInput = await page.waitForSelector(PIN_SEL, { timeout: 30_000 });
|
|
|
} catch {
|
|
} catch {
|
|
|
- throw new Error('[booking-2fa] No PIN input found — 2FA could not be completed automatically');
|
|
|
|
|
|
|
+ console.error('[booking-2fa] No PIN input found — no active 2FA challenge, or challenge not visible');
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
console.error('[booking-2fa] Polling /sms/code for up to 2 minutes...');
|
|
console.error('[booking-2fa] Polling /sms/code for up to 2 minutes...');
|
|
@@ -40,6 +50,7 @@ async function handleAuthAssurance(page: Page, masked: string): Promise<void> {
|
|
|
console.error(`[booking-2fa] Filling code ${code}`);
|
|
console.error(`[booking-2fa] Filling code ${code}`);
|
|
|
await pinInput.fill(code);
|
|
await pinInput.fill(code);
|
|
|
await page.keyboard.press('Enter');
|
|
await page.keyboard.press('Enter');
|
|
|
|
|
+ return true;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export async function triggerChatAuth(mode: string): Promise<CredentialsPayload> {
|
|
export async function triggerChatAuth(mode: string): Promise<CredentialsPayload> {
|
|
@@ -52,8 +63,9 @@ export async function triggerChatAuth(mode: string): Promise<CredentialsPayload>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const profile = profileDir('booking', mode);
|
|
const profile = profileDir('booking', mode);
|
|
|
|
|
+ const headless = !process.env.HEADED;
|
|
|
let smsMasked = '';
|
|
let smsMasked = '';
|
|
|
- const context = await openPersistentContext(profile, true);
|
|
|
|
|
|
|
+ const context = await openPersistentContext(profile, headless);
|
|
|
context.on('response', async res => {
|
|
context.on('response', async res => {
|
|
|
if (res.url().includes('challenge-methods')) {
|
|
if (res.url().includes('challenge-methods')) {
|
|
|
try {
|
|
try {
|
|
@@ -67,21 +79,57 @@ export async function triggerChatAuth(mode: string): Promise<CredentialsPayload>
|
|
|
const page = context.pages()[0] ?? await context.newPage();
|
|
const page = context.pages()[0] ?? await context.newPage();
|
|
|
const messagingUrl = `https://${ADMIN_BASE}/hotel/hoteladmin/extranet_ng/manage/messaging/inbox.html?hotel_id=${hotelId}&lang=en&ses=${ses}&source=nav&tlc=1`;
|
|
const messagingUrl = `https://${ADMIN_BASE}/hotel/hoteladmin/extranet_ng/manage/messaging/inbox.html?hotel_id=${hotelId}&lang=en&ses=${ses}&source=nav&tlc=1`;
|
|
|
|
|
|
|
|
|
|
+ // Capture challenge-methods response before handleAuthAssurance needs it
|
|
|
|
|
+ const challengeMethodsReady = new Promise<void>(resolve => {
|
|
|
|
|
+ context.once('response', async res => {
|
|
|
|
|
+ if (res.url().includes('challenge-methods')) resolve();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
console.error('[booking-2fa] Navigating to messaging inbox (headless)...');
|
|
console.error('[booking-2fa] Navigating to messaging inbox (headless)...');
|
|
|
await page.goto(messagingUrl);
|
|
await page.goto(messagingUrl);
|
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForLoadState('domcontentloaded');
|
|
|
|
|
+ console.error(`[booking-2fa] Landed on: ${page.url()}`);
|
|
|
|
|
|
|
|
- if (!page.url().includes(ADMIN_BASE)) {
|
|
|
|
|
|
|
+ const landedUrl = page.url();
|
|
|
|
|
+ const onAuthAssurance = landedUrl.includes('auth-assurance');
|
|
|
|
|
+ const onAdmin = landedUrl.includes(ADMIN_BASE);
|
|
|
|
|
+ if (!onAdmin && !onAuthAssurance) {
|
|
|
await context.close();
|
|
await context.close();
|
|
|
- throw new Error(`Session invalid — redirected to ${page.url()} — run refresh-creds first`);
|
|
|
|
|
|
|
+ throw new Error(`Session invalid — redirected to ${landedUrl} — run refresh-creds first`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (page.url().includes('auth-assurance')) {
|
|
|
|
|
- await handleAuthAssurance(page, smsMasked);
|
|
|
|
|
- await page.waitForURL(`**${ADMIN_BASE}/**`, { timeout: 120_000 });
|
|
|
|
|
- console.error('[booking-2fa] Auth-assurance completed.');
|
|
|
|
|
- } else {
|
|
|
|
|
|
|
+ // Wait for the MFE JS to trigger auth-assurance and challenge-methods to fire,
|
|
|
|
|
+ // or bail after 10s if no challenge appears.
|
|
|
|
|
+ await Promise.race([
|
|
|
|
|
+ challengeMethodsReady,
|
|
|
|
|
+ page.waitForURL('**auth-assurance**', { timeout: 10_000 }).catch(() => {}),
|
|
|
|
|
+ new Promise<void>(r => setTimeout(r, 10_000)),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ console.error(`[booking-2fa] After wait — url: ${page.url()}, masked: ${smsMasked || '(none)'}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (!page.url().includes('auth-assurance')) {
|
|
|
console.error('[booking-2fa] No auth-assurance challenge — messaging already primed for this session.');
|
|
console.error('[booking-2fa] No auth-assurance challenge — messaging already primed for this session.');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Wait for the auth-assurance page to render — options are links, not buttons.
|
|
|
|
|
+ await page.waitForSelector('text=Text message', { timeout: 30_000 }).catch(() => {});
|
|
|
|
|
+ const completed = await handleAuthAssurance(page, smsMasked);
|
|
|
|
|
+ if (completed) {
|
|
|
|
|
+ if (page.url().includes('auth-assurance')) {
|
|
|
|
|
+ await page.waitForURL(`**${ADMIN_BASE}/**`, { timeout: 120_000 });
|
|
|
|
|
+ }
|
|
|
|
|
+ console.error('[booking-2fa] Auth-assurance completed.');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Diagnostic screenshot before closing
|
|
|
|
|
+ const screenshotPath = path.join(DATA_HOME, 'booking-2fa-debug.png');
|
|
|
|
|
+ await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
|
|
|
|
|
+ console.error(`[booking-2fa] Screenshot: ${screenshotPath}`);
|
|
|
|
|
+
|
|
|
|
|
+ if (!headless) {
|
|
|
|
|
+ console.error('[booking-2fa] Headed mode — press Enter to close the browser...');
|
|
|
|
|
+ await new Promise<void>(resolve => process.stdin.once('data', () => resolve()));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const all = await context.cookies();
|
|
const all = await context.cookies();
|