@agent-native/creative-context 0.5.12 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { existsSync } from "node:fs";
2
2
  import { isBlockedExtensionUrlWithDns, ssrfSafeFetch, } from "@agent-native/core/extensions/url-safety";
3
3
  import { extractStaticWebsiteContext, rankColorSamples, readBoundedResponseBytes, } from "@agent-native/core/ingestion";
4
4
  import { normalizeWhitespace } from "./normalize.js";
@@ -12,75 +12,43 @@ const MAX_TYPOGRAPHY = 100;
12
12
  const MAX_SPACING = 100;
13
13
  const MAX_RADII = 64;
14
14
  const MAX_CSS_VARIABLES = 128;
15
+ const MAX_COMPONENT_STYLES = 24;
16
+ const FONT_READY_TIMEOUT_MS = 4_000;
17
+ const BROWSER_NAVIGATION_DISABLED_WARNING = "Browser rendering is disabled until Chromium navigation has a connect-time SSRF guard.";
15
18
  export class LayeredRenderedPageProvider {
16
- #requestBuilderBrowserConnection;
17
- #loadPlaywright;
18
- #requestAttachedBrowserConnection;
19
19
  #staticFetch;
20
20
  constructor(options = {}) {
21
- this.#requestBuilderBrowserConnection =
22
- options.requestBuilderBrowserConnection ?? defaultBuilderBrowserRequest;
23
- this.#loadPlaywright = options.loadPlaywright ?? loadOptionalPlaywright;
24
- this.#requestAttachedBrowserConnection =
25
- options.requestAttachedBrowserConnection;
26
21
  this.#staticFetch = options.staticFetch ?? ssrfSafeFetch;
27
22
  }
28
23
  async render(request) {
29
24
  await assertPublicBrowserUrl(request.url);
30
- const warnings = [];
31
- const playwright = await this.#loadPlaywright().catch((error) => {
32
- warnings.push(`Playwright unavailable: ${errorMessage(error)}`);
33
- return null;
34
- });
35
- if (request.preferHosted !== false && playwright) {
36
- try {
37
- const connection = await this.#requestBuilderBrowserConnection({
38
- sessionId: `creative-context-${randomUUID()}`,
39
- });
40
- const wsUrl = stringValue(connection.wsUrl);
41
- if (!wsUrl)
42
- throw new Error("Builder Browser did not return wsUrl.");
43
- return await renderWithPlaywright(playwright, request, warnings, "builder-browser", wsUrl);
44
- }
45
- catch (error) {
46
- warnings.push(`Builder Browser unavailable: ${errorMessage(error)}`);
47
- }
48
- }
49
- if (playwright) {
50
- try {
51
- return await renderWithPlaywright(playwright, request, warnings, "local-playwright");
52
- }
53
- catch (error) {
54
- warnings.push(`Local Playwright unavailable: ${errorMessage(error)}`);
55
- }
56
- }
57
- if (playwright && this.#requestAttachedBrowserConnection) {
58
- try {
59
- const connection = await this.#requestAttachedBrowserConnection({
60
- sessionId: `creative-context-attached-${randomUUID()}`,
61
- url: request.url,
62
- });
63
- if (!connection.wsUrl?.trim()) {
64
- throw new Error("Approved attached browser did not return wsUrl.");
65
- }
66
- return await renderWithPlaywright(playwright, request, warnings, "attached-chrome", connection.wsUrl);
67
- }
68
- catch (error) {
69
- warnings.push(`Attached Chrome unavailable: ${errorMessage(error)}`);
70
- }
71
- }
72
- else {
73
- warnings.push("Attached Chrome unavailable: no approved browser connection adapter is configured.");
74
- }
75
- return renderStatic(request, warnings, this.#staticFetch);
25
+ return renderStatic(request, [BROWSER_NAVIGATION_DISABLED_WARNING], this.#staticFetch);
76
26
  }
77
27
  }
78
- async function renderWithPlaywright(playwright, request, warnings, method, wsUrl) {
28
+ /**
29
+ * Internal browser renderer retained for a future connect-time-guarded
30
+ * adapter. `LayeredRenderedPageProvider` must not call this for arbitrary
31
+ * caller URLs until Chromium's socket connection is guarded as well.
32
+ */
33
+ export async function renderWithPlaywright(playwright, request, warnings, method, wsUrl) {
79
34
  const browser = wsUrl
80
35
  ? await playwright.chromium.connectOverCDP(wsUrl)
81
- : await playwright.chromium.launch({ headless: true });
36
+ : await launchChromium(playwright.chromium);
37
+ let isolatedContext;
82
38
  try {
83
- const context = browser.contexts()[0] ?? (await browser.newContext());
39
+ // A hosted CDP browser can already contain the user's other tabs. Prefer a
40
+ // fresh context so extraction never reads ambient browser state, cookies,
41
+ // or a page left behind by another workflow. Older browser adapters may not
42
+ // expose newContext, so retain the existing-context fallback.
43
+ try {
44
+ isolatedContext = await browser.newContext?.();
45
+ }
46
+ catch {
47
+ isolatedContext = undefined;
48
+ }
49
+ const context = isolatedContext ?? browser.contexts()[0];
50
+ if (!context)
51
+ throw new Error("Browser did not provide an isolated context.");
84
52
  const page = context.pages()[0] ?? (await context.newPage());
85
53
  await installNavigationGuard(page);
86
54
  await page.setViewportSize({ width: 1440, height: 900 });
@@ -88,27 +56,60 @@ async function renderWithPlaywright(playwright, request, warnings, method, wsUrl
88
56
  timeout: boundedTimeout(request.timeoutMs),
89
57
  waitUntil: request.waitUntil ?? "domcontentloaded",
90
58
  });
59
+ await page
60
+ .waitForLoadState?.("load", {
61
+ timeout: Math.min(8_000, boundedTimeout(request.timeoutMs)),
62
+ })
63
+ .catch((error) => {
64
+ warnings.push(`Browser load stabilization unavailable: ${errorMessage(error)}`);
65
+ });
66
+ // React hydration, CSS-in-JS insertion, and web fonts commonly finish just
67
+ // after `load`. Give those layers a bounded chance to settle, then capture
68
+ // the computed cascade rather than the server HTML.
69
+ await page
70
+ .waitForLoadState?.("networkidle", { timeout: 4_000 })
71
+ .catch((error) => {
72
+ warnings.push(`Browser network-idle stabilization unavailable: ${errorMessage(error)}`);
73
+ });
74
+ await waitForFontReadiness(page, Math.min(FONT_READY_TIMEOUT_MS, boundedTimeout(request.timeoutMs))).catch((error) => {
75
+ warnings.push(`Browser font readiness unavailable: ${errorMessage(error)}`);
76
+ });
77
+ await new Promise((resolve) => setTimeout(resolve, 150));
78
+ await page.evaluate(dismissConsentOverlays).catch(() => undefined);
91
79
  const finalUrl = page.url();
92
80
  await assertPublicBrowserUrl(finalUrl);
93
81
  const [title, text, desktopScreenshot, extraction] = await Promise.all([
94
- page.title().catch(() => ""),
82
+ page.title().catch((error) => {
83
+ warnings.push(`Browser title extraction unavailable: ${errorMessage(error)}`);
84
+ return "";
85
+ }),
95
86
  page
96
87
  .locator("body")
97
88
  .innerText()
98
- .catch(() => ""),
89
+ .catch((error) => {
90
+ warnings.push(`Browser text extraction unavailable: ${errorMessage(error)}`);
91
+ return "";
92
+ }),
99
93
  page
100
94
  .screenshot({ type: "png", fullPage: false })
101
95
  .then(boundedScreenshot)
102
- .catch(() => undefined),
103
- page
104
- .evaluate(captureRenderedWebsiteContext)
105
- .catch(() => emptyExtraction()),
96
+ .catch((error) => {
97
+ warnings.push(`Desktop screenshot unavailable: ${errorMessage(error)}`);
98
+ return undefined;
99
+ }),
100
+ page.evaluate(captureRenderedWebsiteContext).catch((error) => {
101
+ warnings.push(`Browser style extraction unavailable: ${errorMessage(error)}`);
102
+ return emptyExtraction();
103
+ }),
106
104
  ]);
107
105
  await page.setViewportSize({ width: 390, height: 844 });
108
106
  const mobileScreenshot = await page
109
107
  .screenshot({ type: "png", fullPage: false })
110
108
  .then(boundedScreenshot)
111
- .catch(() => undefined);
109
+ .catch((error) => {
110
+ warnings.push(`Mobile screenshot unavailable: ${errorMessage(error)}`);
111
+ return undefined;
112
+ });
112
113
  const unboundedText = normalizeWhitespace(text);
113
114
  const textTruncated = unboundedText.length > MAX_RENDERED_TEXT_CHARS;
114
115
  const normalizedText = unboundedText.slice(0, MAX_RENDERED_TEXT_CHARS);
@@ -173,9 +174,32 @@ async function renderWithPlaywright(playwright, request, warnings, method, wsUrl
173
174
  };
174
175
  }
175
176
  finally {
177
+ await isolatedContext?.close?.().catch((error) => {
178
+ warnings.push(`Browser context cleanup unavailable: ${errorMessage(error)}`);
179
+ });
176
180
  await browser.close().catch(() => undefined);
177
181
  }
178
182
  }
183
+ async function waitForFontReadiness(page, timeoutMs) {
184
+ let timeout;
185
+ try {
186
+ await Promise.race([
187
+ page.evaluate(async () => {
188
+ if (document.fonts?.ready)
189
+ await document.fonts.ready;
190
+ }),
191
+ new Promise((_, reject) => {
192
+ timeout = setTimeout(() => {
193
+ reject(new Error(`font readiness timed out after ${timeoutMs}ms`));
194
+ }, timeoutMs);
195
+ }),
196
+ ]);
197
+ }
198
+ finally {
199
+ if (timeout !== undefined)
200
+ clearTimeout(timeout);
201
+ }
202
+ }
179
203
  async function installNavigationGuard(page) {
180
204
  await page.route("**/*", async (route) => {
181
205
  const request = route.request();
@@ -205,6 +229,69 @@ async function installNavigationGuard(page) {
205
229
  await route.continue();
206
230
  });
207
231
  }
232
+ const SYSTEM_CHROME_EXECUTABLES = [
233
+ "/usr/bin/google-chrome-stable",
234
+ "/usr/bin/google-chrome",
235
+ "/usr/bin/chromium",
236
+ "/usr/bin/chromium-browser",
237
+ ];
238
+ async function launchChromium(chromium) {
239
+ const launchOptions = {
240
+ headless: true,
241
+ args: ["--no-sandbox", "--disable-dev-shm-usage"],
242
+ };
243
+ try {
244
+ return await chromium.launch(launchOptions);
245
+ }
246
+ catch (error) {
247
+ if (!isMissingBrowserError(error))
248
+ throw error;
249
+ for (const executablePath of SYSTEM_CHROME_EXECUTABLES) {
250
+ if (!existsSync(executablePath))
251
+ continue;
252
+ try {
253
+ return await chromium.launch({ ...launchOptions, executablePath });
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+ function isMissingBrowserError(error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(message);
265
+ }
266
+ /** Close common consent banners without accepting tracking or changing page data. */
267
+ function dismissConsentOverlays() {
268
+ const selectors = [
269
+ '[aria-label*="reject" i]',
270
+ '[aria-label*="decline" i]',
271
+ '[aria-label*="close" i]',
272
+ '[data-testid*="reject" i]',
273
+ '[data-testid*="decline" i]',
274
+ '[data-testid*="close" i]',
275
+ 'button[id*="reject" i]',
276
+ 'button[id*="decline" i]',
277
+ 'button[class*="reject" i]',
278
+ 'button[class*="decline" i]',
279
+ ];
280
+ for (const selector of selectors) {
281
+ const element = document.querySelector(selector);
282
+ if (!element)
283
+ continue;
284
+ const rect = element.getBoundingClientRect();
285
+ const style = getComputedStyle(element);
286
+ if (rect.width > 0 &&
287
+ rect.height > 0 &&
288
+ style.visibility !== "hidden" &&
289
+ style.display !== "none") {
290
+ element.click();
291
+ return;
292
+ }
293
+ }
294
+ }
208
295
  async function renderStatic(request, warnings, fetcher) {
209
296
  const response = await fetcher(request.url, {
210
297
  headers: {
@@ -264,22 +351,6 @@ async function assertPublicBrowserUrl(value) {
264
351
  throw new Error("SSRF blocked: website resolved to a private/internal host.");
265
352
  }
266
353
  }
267
- async function defaultBuilderBrowserRequest(input) {
268
- const server = (await import("@agent-native/core/server"));
269
- if (!server.requestBuilderBrowserConnection) {
270
- throw new Error("@agent-native/core/server does not export requestBuilderBrowserConnection.");
271
- }
272
- return server.requestBuilderBrowserConnection(input);
273
- }
274
- async function loadOptionalPlaywright() {
275
- const specifier = "playwright";
276
- try {
277
- return (await import(specifier));
278
- }
279
- catch {
280
- return null;
281
- }
282
- }
283
354
  function captureRenderedWebsiteContext() {
284
355
  const MAX_ASSETS = 500;
285
356
  const MAX_LINKS = 500;
@@ -287,15 +358,148 @@ function captureRenderedWebsiteContext() {
287
358
  const MAX_TYPE_STYLES = 100;
288
359
  const MAX_SPACING_VALUES = 100;
289
360
  const MAX_RADIUS_VALUES = 64;
361
+ const MAX_SHADOWS = 32;
362
+ const MAX_BACKGROUNDS = 32;
290
363
  const MAX_VARIABLES = 128;
291
364
  const MAX_TEXT = 2_000_000;
365
+ const MAX_COMPONENT_STYLES = 24;
292
366
  const assets = new Map();
293
367
  const links = new Set();
294
368
  const colors = [];
295
369
  const typography = new Map();
296
370
  const spacing = new Set();
297
371
  const radii = new Set();
372
+ const shadows = new Set();
373
+ const backgrounds = new Set();
374
+ const components = [];
298
375
  const cssVariables = {};
376
+ const semanticColors = {};
377
+ const isOpaque = (value) => {
378
+ const normalized = value.trim().toLowerCase();
379
+ if (!normalized || normalized === "transparent")
380
+ return false;
381
+ const functionBody = normalized.match(/^[a-z-]+\((.*)\)$/)?.[1];
382
+ if (!functionBody)
383
+ return true;
384
+ const alphaValue = functionBody.includes("/")
385
+ ? functionBody.split("/").at(-1)?.trim()
386
+ : functionBody.split(",").length === 4
387
+ ? functionBody.split(",").at(-1)?.trim()
388
+ : undefined;
389
+ if (!alphaValue)
390
+ return true;
391
+ const alpha = alphaValue.endsWith("%")
392
+ ? Number.parseFloat(alphaValue) / 100
393
+ : Number.parseFloat(alphaValue);
394
+ return Number.isNaN(alpha) || alpha > 0.02;
395
+ };
396
+ const addColor = (value) => {
397
+ if (colors.length >= MAX_COLOR_VALUES || !isOpaque(value))
398
+ return;
399
+ const normalized = value.trim();
400
+ if (!colors.includes(normalized))
401
+ colors.push(normalized);
402
+ };
403
+ const visible = (element) => {
404
+ const style = getComputedStyle(element);
405
+ const rect = element.getBoundingClientRect();
406
+ return (rect.width > 0 &&
407
+ rect.height > 0 &&
408
+ style.display !== "none" &&
409
+ style.visibility !== "hidden" &&
410
+ Number(style.opacity || 1) > 0.02);
411
+ };
412
+ const firstVisible = (selector) => Array.from(document.querySelectorAll(selector)).find(visible);
413
+ const opaqueValue = (value) => isOpaque(value) ? value.trim() : undefined;
414
+ const recordComputedStyle = (element, role) => {
415
+ const style = getComputedStyle(element);
416
+ const values = [
417
+ style.color,
418
+ style.backgroundColor,
419
+ style.borderTopColor,
420
+ style.borderRightColor,
421
+ style.borderBottomColor,
422
+ style.borderLeftColor,
423
+ ];
424
+ values.forEach(addColor);
425
+ if (style.boxShadow && style.boxShadow !== "none") {
426
+ if (shadows.size < MAX_SHADOWS)
427
+ shadows.add(style.boxShadow);
428
+ }
429
+ if (style.backgroundImage && style.backgroundImage !== "none") {
430
+ if (backgrounds.size < MAX_BACKGROUNDS) {
431
+ backgrounds.add(style.backgroundImage);
432
+ }
433
+ }
434
+ for (const value of [
435
+ style.marginTop,
436
+ style.marginRight,
437
+ style.marginBottom,
438
+ style.marginLeft,
439
+ style.paddingTop,
440
+ style.paddingRight,
441
+ style.paddingBottom,
442
+ style.paddingLeft,
443
+ style.gap,
444
+ style.rowGap,
445
+ style.columnGap,
446
+ ]) {
447
+ if (spacing.size < MAX_SPACING_VALUES &&
448
+ value &&
449
+ value !== "0px" &&
450
+ value !== "normal") {
451
+ spacing.add(value);
452
+ }
453
+ }
454
+ for (const value of [
455
+ style.borderTopLeftRadius,
456
+ style.borderTopRightRadius,
457
+ style.borderBottomRightRadius,
458
+ style.borderBottomLeftRadius,
459
+ ]) {
460
+ if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
461
+ radii.add(value);
462
+ }
463
+ }
464
+ const type = {
465
+ family: style.fontFamily,
466
+ size: style.fontSize,
467
+ weight: style.fontWeight,
468
+ lineHeight: style.lineHeight,
469
+ letterSpacing: style.letterSpacing,
470
+ };
471
+ if (typography.size < MAX_TYPE_STYLES && type.family) {
472
+ typography.set(JSON.stringify(type), type);
473
+ }
474
+ if (!role || components.length >= MAX_COMPONENT_STYLES)
475
+ return style;
476
+ components.push({
477
+ role,
478
+ fontFamily: style.fontFamily,
479
+ fontSize: style.fontSize,
480
+ fontWeight: style.fontWeight,
481
+ lineHeight: style.lineHeight,
482
+ letterSpacing: style.letterSpacing,
483
+ color: opaqueValue(style.color),
484
+ backgroundColor: opaqueValue(style.backgroundColor),
485
+ backgroundImage: style.backgroundImage !== "none" ? style.backgroundImage : undefined,
486
+ border: style.borderStyle !== "none" && style.borderWidth !== "0px"
487
+ ? style.border
488
+ : undefined,
489
+ borderRadius: style.borderTopLeftRadius !== "0px" ? style.borderRadius : undefined,
490
+ boxShadow: style.boxShadow !== "none" ? style.boxShadow : undefined,
491
+ padding: style.padding !== "0px" ? style.padding : undefined,
492
+ gap: style.gap !== "normal" ? style.gap : undefined,
493
+ textTransform: style.textTransform !== "none" ? style.textTransform : undefined,
494
+ });
495
+ return style;
496
+ };
497
+ const styleFor = (selector, role) => {
498
+ const element = firstVisible(selector);
499
+ if (!element)
500
+ return undefined;
501
+ return recordComputedStyle(element, role);
502
+ };
299
503
  const addAsset = (raw, kind, role) => {
300
504
  if (!raw || assets.size >= MAX_ASSETS)
301
505
  return;
@@ -374,52 +578,67 @@ function captureRenderedWebsiteContext() {
374
578
  }
375
579
  }
376
580
  }
377
- const elements = Array.from(document.querySelectorAll("body *")).slice(0, 500);
581
+ const bodyStyle = document.body
582
+ ? recordComputedStyle(document.body)
583
+ : undefined;
584
+ const rootBackground = opaqueValue(rootStyle.backgroundColor);
585
+ const bodyBackground = bodyStyle
586
+ ? opaqueValue(bodyStyle.backgroundColor)
587
+ : undefined;
588
+ const headingStyle = styleFor("h1, h2, h3", "heading");
589
+ const textStyle = styleFor("p, li, label, body", "body");
590
+ const buttonStyle = styleFor('button, [role="button"], input[type="submit"], a[class*="button" i], a[class*="cta" i]', "button");
591
+ const linkStyle = styleFor("a[href]", "link");
592
+ const cardStyle = styleFor('article, [class*="card" i], [class*="panel" i], [class*="surface" i], section', "card");
593
+ styleFor("input, textarea, select", "input");
594
+ styleFor("nav, header", "nav");
595
+ styleFor('main, [class*="hero" i]', "hero");
596
+ const setSemantic = (role, value) => {
597
+ if (value)
598
+ semanticColors[role] = value;
599
+ };
600
+ setSemantic("background", bodyBackground ?? rootBackground);
601
+ setSemantic("surface", cardStyle ? opaqueValue(cardStyle.backgroundColor) : undefined);
602
+ setSemantic("text", textStyle ? opaqueValue(textStyle.color) : undefined);
603
+ const mutedElement = firstVisible("small, figcaption, [class*='muted' i]");
604
+ setSemantic("textMuted", mutedElement
605
+ ? opaqueValue(getComputedStyle(mutedElement).color)
606
+ : undefined);
607
+ setSemantic("accent", linkStyle
608
+ ? opaqueValue(linkStyle.color)
609
+ : buttonStyle
610
+ ? opaqueValue(buttonStyle.backgroundColor)
611
+ : undefined);
612
+ setSemantic("primary", buttonStyle
613
+ ? (opaqueValue(buttonStyle.backgroundColor) ??
614
+ opaqueValue(buttonStyle.color))
615
+ : colors[0]);
616
+ setSemantic("secondary", semanticColors.surface ?? colors[1] ?? semanticColors.background);
617
+ const layoutElement = firstVisible("main, [role='main'], body > div");
618
+ const layoutStyle = layoutElement
619
+ ? getComputedStyle(layoutElement)
620
+ : undefined;
621
+ const layoutRect = layoutElement?.getBoundingClientRect();
622
+ const layout = {
623
+ contentWidth: layoutStyle?.maxWidth && layoutStyle.maxWidth !== "none"
624
+ ? layoutStyle.maxWidth
625
+ : layoutRect && layoutRect.width > 0
626
+ ? `${Math.round(layoutRect.width)}px`
627
+ : undefined,
628
+ pagePadding: bodyStyle?.padding && bodyStyle.padding !== "0px"
629
+ ? bodyStyle.padding
630
+ : undefined,
631
+ sectionGap: firstVisible("section, article") &&
632
+ getComputedStyle(firstVisible("section, article")).gap !== "normal" &&
633
+ getComputedStyle(firstVisible("section, article")).gap !== "0px"
634
+ ? getComputedStyle(firstVisible("section, article")).gap
635
+ : undefined,
636
+ };
637
+ const elements = Array.from(document.querySelectorAll("body *"))
638
+ .filter(visible)
639
+ .slice(0, 700);
378
640
  for (const element of elements) {
379
- const style = getComputedStyle(element);
380
- if (colors.length < MAX_COLOR_VALUES) {
381
- colors.push(style.color, style.backgroundColor, style.borderTopColor, style.borderRightColor, style.borderBottomColor, style.borderLeftColor);
382
- }
383
- const type = {
384
- family: style.fontFamily,
385
- size: style.fontSize,
386
- weight: style.fontWeight,
387
- lineHeight: style.lineHeight,
388
- letterSpacing: style.letterSpacing,
389
- };
390
- if (typography.size < MAX_TYPE_STYLES) {
391
- typography.set(JSON.stringify(type), type);
392
- }
393
- for (const value of [
394
- style.marginTop,
395
- style.marginRight,
396
- style.marginBottom,
397
- style.marginLeft,
398
- style.paddingTop,
399
- style.paddingRight,
400
- style.paddingBottom,
401
- style.paddingLeft,
402
- style.gap,
403
- style.rowGap,
404
- style.columnGap,
405
- ]) {
406
- if (spacing.size < MAX_SPACING_VALUES &&
407
- value &&
408
- value !== "0px" &&
409
- value !== "normal") {
410
- spacing.add(value);
411
- }
412
- }
413
- for (const value of [
414
- style.borderTopLeftRadius,
415
- style.borderTopRightRadius,
416
- style.borderBottomRightRadius,
417
- style.borderBottomLeftRadius,
418
- ]) {
419
- if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
420
- radii.add(value);
421
- }
422
- }
641
+ recordComputedStyle(element);
423
642
  }
424
643
  return {
425
644
  title: document.title,
@@ -432,10 +651,21 @@ function captureRenderedWebsiteContext() {
432
651
  spacing: [...spacing],
433
652
  radii: [...radii],
434
653
  cssVariables,
654
+ semanticColors,
655
+ shadows: [...shadows],
656
+ backgrounds: [...backgrounds],
657
+ components,
658
+ layout,
435
659
  },
436
660
  };
437
661
  }
438
662
  export function boundWebsiteExtraction(extraction) {
663
+ const boundedComponents = extraction.designTokens.components
664
+ ?.slice(0, MAX_COMPONENT_STYLES)
665
+ .map((component) => Object.fromEntries(Object.entries(component).map(([key, value]) => [
666
+ key,
667
+ typeof value === "string" ? value.slice(0, 500) : value,
668
+ ])));
439
669
  return {
440
670
  title: normalizeWhitespace(extraction.title).slice(0, 500),
441
671
  text: normalizeWhitespace(extraction.text).slice(0, MAX_RENDERED_TEXT_CHARS),
@@ -454,6 +684,35 @@ export function boundWebsiteExtraction(extraction) {
454
684
  cssVariables: Object.fromEntries(Object.entries(extraction.designTokens.cssVariables)
455
685
  .slice(0, MAX_CSS_VARIABLES)
456
686
  .map(([name, value]) => [name.slice(0, 500), value.slice(0, 4_096)])),
687
+ ...(extraction.designTokens.semanticColors
688
+ ? {
689
+ semanticColors: Object.fromEntries(Object.entries(extraction.designTokens.semanticColors)
690
+ .filter(([, value]) => typeof value === "string" && value)
691
+ .map(([name, value]) => [name, value.slice(0, 200)])),
692
+ }
693
+ : {}),
694
+ ...(extraction.designTokens.shadows
695
+ ? {
696
+ shadows: extraction.designTokens.shadows
697
+ .slice(0, 32)
698
+ .map((value) => value.slice(0, 500)),
699
+ }
700
+ : {}),
701
+ ...(extraction.designTokens.backgrounds
702
+ ? {
703
+ backgrounds: extraction.designTokens.backgrounds
704
+ .slice(0, 32)
705
+ .map((value) => value.slice(0, 500)),
706
+ }
707
+ : {}),
708
+ ...(boundedComponents ? { components: boundedComponents } : {}),
709
+ ...(extraction.designTokens.layout
710
+ ? {
711
+ layout: Object.fromEntries(Object.entries(extraction.designTokens.layout)
712
+ .filter(([, value]) => typeof value === "string" && value)
713
+ .map(([name, value]) => [name, value.slice(0, 200)])),
714
+ }
715
+ : {}),
457
716
  },
458
717
  };
459
718
  }
@@ -495,9 +754,6 @@ function boundedTimeout(value) {
495
754
  function boundedScreenshot(value) {
496
755
  return value.byteLength <= MAX_SCREENSHOT_BYTES ? value : undefined;
497
756
  }
498
- function stringValue(value) {
499
- return typeof value === "string" && value.trim() ? value.trim() : undefined;
500
- }
501
757
  function errorMessage(error) {
502
758
  return error instanceof Error ? error.message : String(error);
503
759
  }