@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
JavaScript
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, '&').replace(/"/g, '"').replace(/</g, '<');
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