UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

141 lines • 6.99 kB
import { boundedCaptureOptions } from './server.js'; export function createPlaywrightCaptureAdapter(options = {}) { async function withPage(settings, operation) { const limits = boundedCaptureOptions(settings); const browser = options.browser ?? (await (await import('playwright-core')).chromium.launch(options.launchOptions)); try { const context = await browser.newContext({ javaScriptEnabled: false, serviceWorkers: 'block', viewport: { width: limits.width, height: limits.height }, }); try { const page = await context.newPage(); try { page.setDefaultTimeout(limits.timeoutMs); await context.route('**/*', async (route) => { try { if (!options.allowAsset?.(route.request().url())) { await route.abort(); return; } // Browser redirects bypass route handlers. Fetch one approved URL only, // then fulfill its response; never let an asset redirect escape the allowlist. const response = await route.fetch({ maxRedirects: 0, timeout: limits.timeoutMs }); try { if (response.status() >= 300 && response.status() < 400) { await route.abort(); } else { await route.fulfill({ response }); } } finally { await response.dispose(); } } catch { await route.abort(); } }); let timer; try { return await Promise.race([ operation(page, limits), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('genui: capture timed out')), limits.timeoutMs); }), ]); } finally { clearTimeout(timer); } } finally { await page.close(); } } finally { await context.close(); } } finally { if (!options.browser) await browser.close(); } } function checkPages(html) { if (!html.length || html.length > 100) throw new RangeError('genui: capture requires 1–100 pages'); } async function ready(page, html, limits) { await page.setContent(html, { waitUntil: 'load', timeout: limits.timeoutMs }); // Host evaluation is available while page-origin JavaScript stays disabled. await page.evaluate(async (timeoutMs) => { let timer; try { await Promise.race([ Promise.all([ document.fonts.ready, ...Array.from(document.images, (image) => image.complete ? Promise.resolve() : new Promise((resolve) => { image.addEventListener('load', () => resolve(), { once: true }); image.addEventListener('error', () => resolve(), { once: true }); })), ]), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('genui: assets timed out')), timeoutMs); }), ]); } finally { clearTimeout(timer); } }, limits.timeoutMs); const height = await page.evaluate(() => Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)); if (height > limits.height) throw new RangeError('genui: content exceeds capture height; reduce rowsPerPage or paginate the component'); } return { async images(html, settings) { checkPages(html); return withPage(settings, async (page, limits) => { const images = []; for (const content of html) { await ready(page, content, limits); images.push(await page.screenshot({ type: 'png', fullPage: true, timeout: limits.timeoutMs })); } return images; }); }, async pdf(html, settings) { checkPages(html); return withPage(settings, async (page, limits) => { // Separate full HTML pages via srcdoc to preserve per-page CSS and theme. // Keep frames in the intercepted origin; opaque sandbox frames can bypass Chromium // request routing. Scripts remain disabled by both the context and the sandbox. const escapeAttribute = (value) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;'); const pdfDocument = `<!doctype html><html><head><style>@page { size: ${limits.width}px ${limits.height}px; margin: 0 } body { margin: 0 } iframe { display:block; border:0; width:${limits.width}px; height:${limits.height}px; break-after:page } iframe:last-child { break-after:auto }</style></head><body>${html.map((content) => `<iframe sandbox="allow-same-origin" srcdoc="${escapeAttribute(content)}"></iframe>`).join('')}</body></html>`; await page.setContent(pdfDocument, { waitUntil: 'load', timeout: limits.timeoutMs }); for (const frame of page.frames().slice(1)) { await frame.evaluate(async () => { await document.fonts.ready; await Promise.all(Array.from(document.images, (image) => image.decode().catch(() => { }))); }); const height = await frame.evaluate(() => document.documentElement.scrollHeight); if (height > limits.height) throw new RangeError('genui: content exceeds PDF page height; reduce rowsPerPage'); } return page.pdf({ width: `${limits.width}px`, height: `${limits.height}px`, printBackground: true, preferCSSPageSize: true, }); }); }, }; } //# sourceMappingURL=playwright.js.map