scripts/capture-manage-2fa-screenshot.mjs
Rama main · réplica de lectura del repositorio original.
import { chromium } from '@playwright/test'
import { createClient } from '@supabase/supabase-js'
import { readFileSync } from 'node:fs'
import { mkdir, unlink } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import keys from '../configs.json' with { type: 'json' }
const baseUrl = process.env.CAPGO_URL || 'http://localhost:5173'
const email = process.env.CAPGO_EMAIL || 'test@capgo.app'
const password = process.env.CAPGO_PASSWORD || 'testtest'
const branch = process.env.BRANCH || 'preprod'
const outputPath = process.argv[2]
const mode = process.argv[3] || 'step2'
if (!outputPath) {
console.error('Usage: bun scripts/capture-manage-2fa-screenshot.mjs <output.webp> [step2|rate-limit|captcha-error]')
process.exit(1)
}
const supabaseUrl = keys.supa_url[branch] || keys.supa_url.preprod
const supabaseAnon = keys.supa_anon[branch] || keys.supa_anon.preprod
const supabaseId = supabaseUrl.split('//')[1].split('.')[0].split(':')[0]
const storageKey = `sb-${supabaseId}-auth-token`
const i18n = JSON.parse(readFileSync(new URL('../messages/en.json', import.meta.url), 'utf8'))
const rateLimitSeconds = 54
const rateLimitError = i18n['email-otp-rate-limit-wait'].replace('{seconds}', String(rateLimitSeconds))
const rateLimitCountdown = i18n['email-otp-rate-limit-countdown'].replace('{seconds}', String(rateLimitSeconds))
const rateLimitButton = i18n['email-otp-send-wait'].replace('{seconds}', String(rateLimitSeconds))
const captchaFail = i18n['captcha-fail']
const sendCodeLabel = i18n['email-otp-send-code']
await mkdir(outputPath.split('/').slice(0, -1).join('/') || '.', { recursive: true })
const supabase = createClient(supabaseUrl, supabaseAnon)
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
if (error || !data.session)
throw new Error(error?.message || 'Login failed')
const browser = await chromium.launch({ headless: true })
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } })
await context.addInitScript((captureMode) => {
let autoCompleteCount = 0
window.turnstile = {
render(_element, options) {
if (autoCompleteCount === 0) {
autoCompleteCount++
queueMicrotask(() => options.callback?.('screenshot-turnstile-token'))
}
return 'screenshot-turnstile-widget'
},
reset() {},
remove() {},
}
}, mode)
const page = await context.newPage()
if (mode === 'rate-limit' || mode === 'captcha-error') {
await page.route('**/auth/v1/otp**', async (route) => {
if (mode === 'rate-limit') {
await route.fulfill({
status: 429,
contentType: 'application/json',
body: JSON.stringify({
code: 'over_email_send_rate_limit',
message: `For security purposes, you can only request this after ${rateLimitSeconds} seconds.`,
}),
})
return
}
await route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({
code: 'captcha_failed',
message: 'captcha protection: request disallowed (no captcha_token found)',
}),
})
})
}
await page.goto(`${baseUrl}/login`, { waitUntil: 'domcontentloaded' })
await page.evaluate(({ storageKey, session }) => {
localStorage.setItem(storageKey, JSON.stringify({
access_token: session.access_token,
refresh_token: session.refresh_token,
expires_at: session.expires_at,
expires_in: session.expires_in,
token_type: session.token_type,
user: session.user,
}))
}, { storageKey, session: data.session })
await page.goto(`${baseUrl}/settings/account/manage-2fa`, { waitUntil: 'domcontentloaded' })
await page.waitForSelector('h2', { timeout: 60000 })
if (mode === 'step2') {
await page.getByRole('button', { name: sendCodeLabel }).waitFor({ timeout: 30000 })
}
else {
await page.getByRole('button', { name: sendCodeLabel }).click()
}
if (mode === 'rate-limit') {
await page.getByRole('alert').filter({ hasText: rateLimitError }).waitFor({ timeout: 15000 })
await page.getByRole('status').filter({ hasText: rateLimitCountdown }).waitFor({ timeout: 15000 })
await page.getByRole('button', { name: rateLimitButton }).waitFor({ timeout: 15000 })
}
else if (mode === 'captcha-error') {
await page.getByText(captchaFail).first().waitFor({ timeout: 15000 })
await page.locator('.max-w-lg.mx-auto h4').filter({ hasText: i18n['2fa-step-captcha'] }).waitFor({ timeout: 15000 })
}
else if (mode === 'step2') {
await page.getByRole('button', { name: sendCodeLabel }).waitFor({ timeout: 15000 })
}
await page.waitForTimeout(500)
const tempPng = `${outputPath}.tmp.png`
if (mode === 'captcha-error')
await page.screenshot({ path: tempPng, type: 'png', fullPage: false })
else
await page.locator('.max-w-lg.mx-auto').first().screenshot({ path: tempPng, type: 'png' })
const ffmpeg = spawnSync('ffmpeg', ['-y', '-i', tempPng, outputPath], { stdio: 'pipe' })
await unlink(tempPng).catch(() => {})
if (ffmpeg.status !== 0)
throw new Error(`ffmpeg failed: ${ffmpeg.stderr?.toString() || ffmpeg.status}`)
console.log(`Saved ${outputPath}`)
await browser.close()