@aglyn/plugins-ai 1.0.0-beta.288 → 1.0.0-beta.290
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.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +3 -2
- package/src/lib/activity/ai-activity-actions.js +3 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +2 -0
- package/src/lib/components/ai-job-requests.js +2 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +4 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +6 -1
- package/src/lib/components/ai-site-import-job.component.js +124 -17
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.d.ts +6 -0
- package/src/lib/components/ai-site-import-start.component.js +18 -4
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +2 -1
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +20 -2
- package/src/lib/jobs/ai-image-reads.js +47 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +53 -3
- package/src/lib/jobs/ai-job-import-step.js +386 -64
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +33 -0
- package/src/lib/jobs/ai-job-layout-language.js +75 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +8 -3
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +29 -0
- package/src/lib/jobs/ai-job-page-language.js +178 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +3 -0
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-site-import-document.d.ts +117 -0
- package/src/lib/jobs/ai-site-import-document.js +710 -0
- package/src/lib/jobs/ai-site-import-document.js.map +1 -0
- package/src/lib/jobs/ai-site-import-look.d.ts +112 -0
- package/src/lib/jobs/ai-site-import-look.js +402 -0
- package/src/lib/jobs/ai-site-import-look.js.map +1 -0
- package/src/lib/jobs/ai-site-import-publish.d.ts +41 -0
- package/src/lib/jobs/ai-site-import-publish.js +92 -0
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -0
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +27 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js +37 -8
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +17 -0
- package/src/lib/layout-language/ai-layout-frame.js +73 -2
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +43 -0
- package/src/lib/layout-language/ai-layout-language.js +199 -0
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +15 -0
- package/src/lib/model/ai-credit-estimate.js +14 -1
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +13 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +47 -3
- package/src/lib/model/ai-site-import-view.js +122 -20
- package/src/lib/model/ai-site-import-view.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +232 -7
- package/src/lib/model/ai-site-import.js +365 -35
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +18 -0
- package/src/lib/runtime/ai-doctrine-validators.js +16 -5
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +31 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.js +24 -1
- package/src/lib/server/ai-image-reads-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +7 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/site-import/fixtures/aglyn-com-recordings.d.ts +57 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js +172 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +32 -7
- package/src/lib/site-import/site-import-crawl.js +133 -44
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-edge-server.d.ts +31 -0
- package/src/lib/site-import/site-import-edge-server.js +31 -0
- package/src/lib/site-import/site-import-edge-server.js.map +1 -0
- package/src/lib/site-import/site-import-edge.d.ts +44 -0
- package/src/lib/site-import/site-import-edge.js +108 -0
- package/src/lib/site-import/site-import-edge.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +73 -3
- package/src/lib/site-import/site-import-html.js +332 -45
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-images.d.ts +22 -0
- package/src/lib/site-import/site-import-images.js +78 -2
- package/src/lib/site-import/site-import-images.js.map +1 -1
- package/src/lib/site-import/site-import-page.d.ts +44 -2
- package/src/lib/site-import/site-import-page.js +116 -16
- package/src/lib/site-import/site-import-page.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-site-import-look.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE SITE BROUGHT IN KEEPS ITS OWN LOOK (AGL-4084).\n *\n * The first wave's look pass, before the header and footer, with no model:\n *\n * - **The theme.** The owner's own home page is read once more as AglynBot\n * (with the edge headers a site this platform serves gets), and the\n * signals a guided start reads off the sites a person likes (AGL-3975:\n * colors, ground, type mood, corners, buttons, cards, header, density,\n * motion) pin the look: its strongest colorful color is the brand, its\n * second the accent. The look is drawn and saved exactly as a guided\n * start's (`aiSiteStyleFor`, `aiSaveSiteTheme`): a built-in base plus the\n * brand colors and a pairing of the source's type mood, never its font.\n * A site whose owner already chose a theme keeps it.\n * - **The logo.** The source header's logo, as the source draws it, into the\n * site's media library once: an SVG is turned into a PNG 192 pixels tall\n * (the library takes pictures, not drawings), a picture is kept as it is.\n * - **The listing.** The home page's own title, description and share\n * picture become the site's listing, only while the site still has the\n * listing it was created with: an owner's own words are never written\n * over.\n *\n * Nothing here fails a wave: a source whose look cannot be read keeps the\n * site's theme, and a logo that cannot be brought in leaves the header its\n * name.\n *=========================================*/\n\nimport type { PluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { aiSiteListingSeeded } from '../model/ai-site-entity'\nimport type { AiSiteImportHome, AiSiteImportLook, AiSiteImportPhoto } from '../model/ai-site-import'\nimport { aiSiteKindFor } from '../model/ai-site-kinds'\nimport { aiSiteSeed, aiSiteStyleFor, type AiSiteStyle } from '../model/ai-site-look'\nimport { aiSiteIsStarterSrc } from '../model/ai-site-photos'\nimport { AGLYN_BOT_USER_AGENT, type SiteImportCrawlSeams } from '../site-import/site-import-crawl'\nimport { bringInSiteImportPhotos, siteImportImageKey, siteImportLogoUrl } from '../site-import/site-import-images'\nimport { aiSaveSiteTheme, aiWorkspaceSiteStyles } from './ai-job-site-look'\nimport { aiSiteInspirationLookPins } from './ai-site-inspiration'\nimport { aiSiteInspirationSignals, type AiSiteInspirationSite } from './ai-site-inspiration-signals'\nimport {\n AI_THEME_BRAND_COLORS_PER_SOURCE,\n AI_THEME_REFERENCE_MAX_BYTES,\n AI_THEME_REFERENCE_STYLESHEETS,\n AI_THEME_STYLESHEET_MAX_BYTES,\n AI_THEME_THEME_COLOR_WEIGHT,\n fetchPublicTextDetailed,\n rankBrandColors,\n stylesheetUrlsOf,\n type AiReferencePage,\n type PublicFetchOptions,\n type PublicFetchResult,\n} from './ai-theme-brand-inputs'\n\n/** The height an SVG logo is drawn at as a picture: crisp at twice the header's 40. */\nexport const AI_SITE_IMPORT_LOGO_HEIGHT = 192\n\n/** The largest logo file read. */\nexport const AI_SITE_IMPORT_LOGO_MAX_BYTES = 1024 * 1024\n\n/** One read's own clock in the look pass. */\nconst LOOK_READ_MS = 8_000\n\ntype Fetch = (url: string, options: PublicFetchOptions) => Promise<PublicFetchResult>\n\nconst sameHost = (a: string, b: string) => {\n try {\n return new URL(a).host === new URL(b).host\n } catch {\n return false\n }\n}\n\n/**\n * The home page and its first stylesheets, read as AglynBot (AGL-4084), the\n * edge headers going with the page and with a stylesheet only on the page's\n * own host. `null` when the page cannot be read.\n */\nexport async function readSiteImportLookPage(\n url: string,\n input: { fetch?: Fetch; headers?: Readonly<Record<string, string>> | null; signal?: AbortSignal },\n): Promise<AiReferencePage | null> {\n const fetchText = input.fetch ?? fetchPublicTextDetailed\n const headers = input.headers && Object.keys(input.headers).length ? input.headers : null\n const read = (address: string, maxBytes: number, accept: RegExp, own: boolean) =>\n fetchText(address, {\n maxBytes,\n accept,\n userAgent: AGLYN_BOT_USER_AGENT,\n timeoutMs: LOOK_READ_MS,\n ...(input.signal ? { signal: input.signal } : {}),\n ...(headers && own ? { headers } : {}),\n }).catch(() => null)\n const page = await read(url, AI_THEME_REFERENCE_MAX_BYTES, /text\\/html|application\\/xhtml\\+xml/i, true)\n if (!page?.ok) return null\n const stylesheets: string[] = []\n for (const sheet of stylesheetUrlsOf(page.text, page.url).slice(0, AI_THEME_REFERENCE_STYLESHEETS)) {\n const css = await read(sheet, AI_THEME_STYLESHEET_MAX_BYTES, /text\\/css/i, sameHost(sheet, page.url))\n if (css?.ok) stylesheets.push(css.text)\n }\n return { url: page.url, html: page.text, stylesheets }\n}\n\nconst SOLID_HEX = /(?<!&)#(?:[0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-z])/gi\nconst SOLID_RGB = /rgba?\\(\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})\\s*(?:[,/]\\s*([\\d.]+%?)\\s*)?\\)/gi\nconst THEME_COLOR = /<meta\\b[^>]*\\bname\\s*=\\s*[\"']?theme-color\\b[^>]*\\bcontent\\s*=\\s*[\"']?\\s*(#[0-9a-f]{3,6})/gi\n\nconst hexOf = (value: string): string | null => {\n const raw = value.trim().toLowerCase()\n if (/^#[0-9a-f]{6}$/.test(raw)) return raw\n if (/^#[0-9a-f]{3}$/.test(raw)) return `#${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`\n return null\n}\n\n/**\n * A source's brand colors as it DRAWS them (AGL-4084): the colors its markup\n * and stylesheets use solid, most used first, the colorful before the greys.\n * A translucent color is a tint of one (a hover, a focus ring), never a use\n * of it, so it is not counted: aglyn.com's focus rings, a light blue at a\n * quarter strength, outnumbered the deep blue its buttons and links are\n * drawn in, which the theme reader would otherwise take as its brand.\n */\nexport function aiSiteImportBrandColors(page: Pick<AiReferencePage, 'html' | 'stylesheets'>): string[] {\n const weights = new Map<string, number>()\n const add = (hex: string | null, weight = 1) => {\n if (hex) weights.set(hex, (weights.get(hex) ?? 0) + weight)\n }\n for (const match of page.html.matchAll(THEME_COLOR)) add(hexOf(match[1] ?? ''), AI_THEME_THEME_COLOR_WEIGHT)\n for (const text of [page.html, ...page.stylesheets]) {\n for (const match of text.matchAll(SOLID_HEX)) add(hexOf(match[0]))\n for (const match of text.matchAll(SOLID_RGB)) {\n const alpha = match[4]\n if (alpha !== undefined && (alpha.endsWith('%') ? Number(alpha.slice(0, -1)) < 100 : Number(alpha) < 1)) continue\n const channels = [match[1], match[2], match[3]].map((value) => Math.min(255, Number(value)))\n add(`#${channels.map((value) => value.toString(16).padStart(2, '0')).join('')}`)\n }\n }\n return rankBrandColors(weights, AI_THEME_BRAND_COLORS_PER_SOURCE)\n}\n\n/**\n * The look a source's signals pin (AGL-4084), drawn the way a guided start's\n * is: the kind of site its own description says, the import's seed, its\n * strongest color as the brand. `null` where the source showed no color to\n * carry a brand, so the site keeps the theme it has.\n */\nexport function aiSiteImportStyle(\n site: AiSiteInspirationSite | null,\n input: { seed: number; description: string; avoid?: readonly AiSiteStyle[] },\n): AiSiteStyle | null {\n if (!site) return null\n const kind = aiSiteKindFor(input.description)\n const pins = aiSiteInspirationLookPins({ v: 1, sites: [site], unread: 0 }, { kind, seed: input.seed })\n if (!pins.brand) return null\n return aiSiteStyleFor({ kind, answer: {}, seed: input.seed, brand: pins.brand, avoid: input.avoid ?? [], pins })\n}\n\n/**\n * The site's listing in the home page's own words (AGL-4084), as field\n * paths: its title and description each only where the site still has the\n * one it was created with, so an owner's own listing is never written over.\n * `{}` with nothing to write.\n */\nexport function aiSiteImportListingUpdates(seo: unknown, displayName: unknown, home: Pick<AiSiteImportHome, 'title' | 'description'>): Record<string, string> {\n const seeded = aiSiteListingSeeded(seo, displayName)\n const current = (seo && typeof seo === 'object' ? seo : {}) as Record<string, unknown>\n const updates: Record<string, string> = {}\n const title = home.title.replace(/\\s+/g, ' ').trim()\n const description = home.description.replace(/\\s+/g, ' ').trim()\n if (title && seeded.title && title !== current['title']) updates['seo.title'] = title\n if (description && seeded.description && description !== current['description']) updates['seo.description'] = description\n return updates\n}\n\n/** Whether the site's share picture is still open: none, or the starter's (AGL-3988). */\nfunction shareOpen(seo: unknown): boolean {\n const image = (seo as { image?: unknown } | null | undefined)?.image\n if (image === undefined || image === null) return true\n return typeof image === 'string' && (!image.trim() || aiSiteIsStarterSrc(image))\n}\n\n/** Turns an SVG drawing into a PNG of a height; `null` where it cannot. */\nexport type AiSiteImportRasterize = (svg: Uint8Array, height: number) => Promise<Uint8Array | null>\n\nexport interface AiSiteImportLookDeps {\n fetch?: Fetch\n headersFor?: SiteImportCrawlSeams['headersFor']\n save?: typeof aiSaveSiteTheme\n others?: typeof aiWorkspaceSiteStyles\n ingest?: PluginMediaIngest | null\n rasterize?: AiSiteImportRasterize\n /** The site's document, as the listing reads it. */\n readHost?: (firestore: FirebaseFirestore.Firestore, hostId: string) => Promise<Record<string, unknown> | null>\n writeHost?: (firestore: FirebaseFirestore.Firestore, hostId: string, updates: Record<string, unknown>) => Promise<void>\n}\n\nexport interface AiSiteImportLookInput {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n orgId: string\n /** The member the logo and the share picture are stored as. */\n uid: string\n /** The import job's id, which the look's seed is drawn from. */\n jobId: string\n domain: string\n home: AiSiteImportHome\n at: string\n signal?: AbortSignal\n}\n\nasync function readHostDoc(firestore: FirebaseFirestore.Firestore, hostId: string): Promise<Record<string, unknown> | null> {\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n return snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n}\n\nasync function writeHostDoc(firestore: FirebaseFirestore.Firestore, hostId: string, updates: Record<string, unknown>): Promise<void> {\n await firestore.collection('hosts').doc(hostId).update(updates)\n}\n\n/** Turns an SVG into a PNG in this process, where `sharp` is shipped; `null` where it is not. */\nasync function rasterizeHere(svg: Uint8Array, height: number): Promise<Uint8Array | null> {\n const images = await import('./ai-image-reads')\n const origin = images.aiImageReadsOrigin()\n if (origin) return images.requestAiLogoRaster(origin, svg, height)\n return images.rasterizeAiLogoSvg(svg, height)\n}\n\n/**\n * The source header's logo in the site's library (AGL-4084), once: the copy\n * a page already brought in is reused by its key. An SVG becomes a PNG.\n * `null` where it cannot be brought in, and the header then prints the name.\n */\nexport async function aiSiteImportLogo(\n input: { hostId: string; uid: string; domain: string; logo: NonNullable<AiSiteImportHome['brand']['logo']>; alt: string; headers?: Readonly<Record<string, string>> | null; signal?: AbortSignal },\n deps: { fetch?: Fetch; ingest: PluginMediaIngest | null; rasterize?: AiSiteImportRasterize },\n): Promise<AiSiteImportPhoto | null> {\n const ingest = deps.ingest\n const url = siteImportLogoUrl(input.logo.url)\n if (!ingest?.findImportedPhoto || !url) return null\n const sourceKey = siteImportImageKey(url)\n try {\n const held = await ingest.findImportedPhoto({ hostId: input.hostId, sourceKey })\n if (held) return { mediaId: held.mediaId, src: held.src, alt: input.alt, from: url, ...(held.width ? { width: held.width } : {}), ...(held.height ? { height: held.height } : {}) }\n const fetched = await (deps.fetch ?? fetchPublicTextDetailed)(url, {\n maxBytes: AI_SITE_IMPORT_LOGO_MAX_BYTES,\n accept: /^image\\//i,\n acceptHeader: 'image/svg+xml,image/png,image/webp,image/jpeg;q=0.9',\n userAgent: AGLYN_BOT_USER_AGENT,\n timeoutMs: LOOK_READ_MS,\n refuseOver: true,\n binary: true,\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.headers && sameHost(url, `https://${input.domain}/`) ? { headers: input.headers } : {}),\n })\n if (!fetched.ok || !fetched.data?.byteLength) return null\n const type = fetched.contentType.split(';')[0].trim().toLowerCase()\n let bytes: Uint8Array | null = fetched.data\n let contentType = type\n if (type === 'image/svg+xml' || input.logo.type === 'svg') {\n bytes = await (deps.rasterize ?? rasterizeHere)(fetched.data, AI_SITE_IMPORT_LOGO_HEIGHT).catch(() => null)\n contentType = 'image/png'\n } else if (!/^image\\/(?:png|jpeg|webp|gif)$/.test(type)) {\n return null\n }\n if (!bytes?.byteLength) return null\n const stored = await ingest.ingest({\n hostId: input.hostId,\n uid: input.uid,\n fileName: `logo.${contentType === 'image/jpeg' ? 'jpg' : contentType.split('/')[1]}`,\n contentType,\n bytes,\n ...(input.alt ? { alt: input.alt.slice(0, 250) } : {}),\n importedFrom: { key: sourceKey, url, domain: input.domain },\n })\n if (!stored.ok) return null\n return { mediaId: stored.mediaId, src: stored.src, alt: input.alt, from: url, ...(stored.width ? { width: stored.width } : {}), ...(stored.height ? { height: stored.height } : {}) }\n } catch (error) {\n console.warn('[site-import] the logo could not be brought in', { hostId: input.hostId, error })\n return null\n }\n}\n\n/** The look pass (AGL-4084): the theme, the logo and the listing, from the owner's own home page. No model. */\nexport async function aiRunSiteImportLook(input: AiSiteImportLookInput, deps: AiSiteImportLookDeps = {}): Promise<AiSiteImportLook> {\n const { firestore, hostId, home } = input\n let headers: Readonly<Record<string, string>> | null = null\n if (deps.headersFor) {\n try {\n headers = await deps.headersFor(input.domain)\n } catch (error) {\n console.warn('[site-import] the edge headers could not be read; reading the look without them', { domain: input.domain, error })\n }\n }\n const seed = aiSiteSeed(input.jobId)\n const page = await readSiteImportLookPage(home.url, { ...(deps.fetch ? { fetch: deps.fetch } : {}), headers, ...(input.signal ? { signal: input.signal } : {}) })\n // The source's own signals, its brand colors as it draws them solid.\n const signals = page ? aiSiteInspirationSignals(page) : null\n const site = signals && page ? { ...signals, colors: aiSiteImportBrandColors(page) } : signals\n const avoid = await (deps.others ?? aiWorkspaceSiteStyles)(firestore, { orgId: input.orgId, hostId })\n const style = aiSiteImportStyle(site, { seed, description: `${home.siteName} ${home.description}`, avoid })\n let theme: AiSiteImportLook['theme'] = 'unread'\n if (style) {\n const saved = await (deps.save ?? aiSaveSiteTheme)(firestore, { hostId, jobId: input.jobId, style })\n if (saved.write === 'missing') throw new Error(`site ${hostId} vanished while its look was brought in`)\n theme = saved.write === 'kept' ? 'kept' : 'applied'\n }\n const ingest = deps.ingest ?? null\n const logo = home.brand.logo\n ? await aiSiteImportLogo(\n { hostId, uid: input.uid, domain: input.domain, logo: home.brand.logo, alt: home.siteName || home.brand.text, headers, ...(input.signal ? { signal: input.signal } : {}) },\n { ingest, ...(deps.fetch ? { fetch: deps.fetch } : {}), ...(deps.rasterize ? { rasterize: deps.rasterize } : {}) },\n )\n : null\n // The site's listing in its own words, only over the one it was created with.\n let seo = false\n const host = await (deps.readHost ?? readHostDoc)(firestore, hostId)\n if (host) {\n const updates: Record<string, unknown> = aiSiteImportListingUpdates(host['seo'], host['displayName'], home)\n if (home.ogImage && shareOpen(host['seo'])) {\n const share = await bringInSiteImportPhotos(\n { hostId, uid: input.uid, domain: input.domain, images: [{ url: home.ogImage, alt: '' }], deadlineAt: Date.now() + LOOK_READ_MS, ...(input.signal ? { signal: input.signal } : {}) },\n { ingest, ...(deps.fetch ? { fetch: deps.fetch } : {}) },\n )\n const photo = share.assets[0]\n if (photo) {\n updates['seo.image'] = photo.src\n if (photo.width) updates['seo.imageWidth'] = photo.width\n if (photo.height) updates['seo.imageHeight'] = photo.height\n }\n }\n if (Object.keys(updates).length) {\n await (deps.writeHost ?? writeHostDoc)(firestore, hostId, updates)\n seo = true\n }\n }\n const drawn = style\n ? {\n ...(typeof style.headerAlign === 'string' ? { headerAlign: style.headerAlign } : {}),\n ...(typeof style.rhythm === 'string' ? { rhythm: style.rhythm } : {}),\n ...(typeof style.motion === 'string' ? { motion: style.motion } : {}),\n }\n : null\n return { at: input.at, theme, colors: site?.colors ?? [], logo, seo, seed: style ? style.seed : seed, style: drawn }\n}\n"],"names":["aiSiteListingSeeded","aiSiteKindFor","aiSiteSeed","aiSiteStyleFor","aiSiteIsStarterSrc","AGLYN_BOT_USER_AGENT","bringInSiteImportPhotos","siteImportImageKey","siteImportLogoUrl","aiSaveSiteTheme","aiWorkspaceSiteStyles","aiSiteInspirationLookPins","aiSiteInspirationSignals","AI_THEME_BRAND_COLORS_PER_SOURCE","AI_THEME_REFERENCE_MAX_BYTES","AI_THEME_REFERENCE_STYLESHEETS","AI_THEME_STYLESHEET_MAX_BYTES","AI_THEME_THEME_COLOR_WEIGHT","fetchPublicTextDetailed","rankBrandColors","stylesheetUrlsOf","AI_SITE_IMPORT_LOGO_HEIGHT","AI_SITE_IMPORT_LOGO_MAX_BYTES","LOOK_READ_MS","sameHost","a","b","URL","host","readSiteImportLookPage","url","input","fetchText","fetch","headers","Object","keys","length","read","address","maxBytes","accept","own","userAgent","timeoutMs","signal","catch","page","ok","stylesheets","sheet","text","slice","css","push","html","SOLID_HEX","SOLID_RGB","THEME_COLOR","hexOf","value","raw","trim","toLowerCase","test","aiSiteImportBrandColors","match","weights","Map","add","hex","weight","set","get","matchAll","alpha","undefined","endsWith","Number","channels","map","Math","min","toString","padStart","join","aiSiteImportStyle","site","kind","description","pins","v","sites","unread","seed","brand","answer","avoid","aiSiteImportListingUpdates","seo","displayName","home","seeded","current","updates","title","replace","shareOpen","image","readHostDoc","firestore","hostId","snapshot","collection","doc","exists","data","writeHostDoc","update","rasterizeHere","svg","height","images","origin","aiImageReadsOrigin","requestAiLogoRaster","rasterizeAiLogoSvg","aiSiteImportLogo","deps","ingest","logo","findImportedPhoto","sourceKey","fetched","held","mediaId","src","alt","from","width","acceptHeader","refuseOver","binary","domain","byteLength","type","contentType","split","bytes","rasterize","stored","uid","fileName","importedFrom","key","error","console","warn","aiRunSiteImportLook","headersFor","jobId","signals","colors","others","orgId","style","siteName","theme","saved","save","write","Error","readHost","ogImage","share","deadlineAt","Date","now","photo","assets","writeHost","drawn","headerAlign","rhythm","motion","at"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { AiJob, AiJobItemLedger, AiJobOutput, AiJobSitePublish } from '../model/ai-jobs.types';
|
|
18
|
+
import type { AiSiteImportState, AiSiteImportWave } from '../model/ai-site-import';
|
|
19
|
+
import { aiPublishGuidedSite } from './ai-site-publish';
|
|
20
|
+
export interface AiSiteImportPublishDeps {
|
|
21
|
+
publish?: typeof aiPublishGuidedSite;
|
|
22
|
+
/** Whether the site is in preview now. */
|
|
23
|
+
inPreview?: (firestore: FirebaseFirestore.Firestore, hostId: string) => Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/** The pages of the import a link may not go to yet: every one not in, the wave's own failures included. */
|
|
26
|
+
export declare function aiSiteImportUnbuilt(state: AiSiteImportState, items: readonly AiJobItemLedger[]): string[];
|
|
27
|
+
/** The wave's pages that came in, as the outputs that publish them, in the wave's order. */
|
|
28
|
+
export declare function aiSiteImportWaveOutputs(state: AiSiteImportState, wave: AiSiteImportWave, items: readonly AiJobItemLedger[], outputs: readonly AiJobOutput[]): AiJobOutput[];
|
|
29
|
+
/**
|
|
30
|
+
* A closed wave's pages, live in the site's preview (AGL-4084). `null` when
|
|
31
|
+
* nothing went live: the site is not in preview, or none of the wave's pages
|
|
32
|
+
* came in. The job's earlier publish is carried in the answer.
|
|
33
|
+
*/
|
|
34
|
+
export declare function aiPublishImportedWave(firestore: FirebaseFirestore.Firestore, input: {
|
|
35
|
+
job: Pick<AiJob, '$id' | 'orgId' | 'hostId'> & Partial<Pick<AiJob, 'inputs' | 'sitePublish'>>;
|
|
36
|
+
state: AiSiteImportState;
|
|
37
|
+
wave: AiSiteImportWave;
|
|
38
|
+
items: readonly AiJobItemLedger[];
|
|
39
|
+
outputs: readonly AiJobOutput[];
|
|
40
|
+
now: Date;
|
|
41
|
+
}, deps?: AiSiteImportPublishDeps): Promise<AiJobSitePublish | null>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /*==========================================
|
|
18
|
+
* EACH WAVE LANDS IN THE SITE'S PREVIEW (AGL-4084).
|
|
19
|
+
*
|
|
20
|
+
* A site brought in starts in preview (AGL-4086, `host.preview`): the public
|
|
21
|
+
* sees its notice, and its team sees the whole site. So when a wave closes,
|
|
22
|
+
* its pages go live INTO THE PREVIEW through the same writes a guided
|
|
23
|
+
* start's publish makes (`aiPublishGuidedSite`): each page takes its
|
|
24
|
+
* address, Home takes `/`, the links to a page still a draft come out, and
|
|
25
|
+
* the cached pages go. The team opens the site and sees the wave; the public
|
|
26
|
+
* still sees the notice until the person makes the site public.
|
|
27
|
+
*
|
|
28
|
+
* Only into a preview. A site that is not in preview keeps the wave's pages
|
|
29
|
+
* as drafts, as every import did before: the public never sees a page no
|
|
30
|
+
* person has looked at. And `site_published` is not sent here: the site is
|
|
31
|
+
* published when its switch makes it public (AGL-4086), not when a wave
|
|
32
|
+
* lands.
|
|
33
|
+
*=========================================*/ import { isHostInPreview } from "@aglyn/aglyn/app-utils/site-preview";
|
|
34
|
+
import { aiMergeSitePublish, aiPublishGuidedSite } from "./ai-site-publish.js";
|
|
35
|
+
async function readInPreview(firestore, hostId) {
|
|
36
|
+
const host = await firestore.collection('hosts').doc(hostId).get();
|
|
37
|
+
return host.exists && isHostInPreview({
|
|
38
|
+
preview: host.get('preview')
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const delivered = (row)=>(row == null ? void 0 : row.status) === 'succeeded' || (row == null ? void 0 : row.status) === 'degraded';
|
|
42
|
+
/** The pages of the import a link may not go to yet: every one not in, the wave's own failures included. */ export function aiSiteImportUnbuilt(state, items) {
|
|
43
|
+
const rows = new Map(items.map((row)=>[
|
|
44
|
+
row.slot,
|
|
45
|
+
row
|
|
46
|
+
]));
|
|
47
|
+
return state.pages.filter((page)=>!delivered(rows.get(page.slot))).map((page)=>page.draftId);
|
|
48
|
+
}
|
|
49
|
+
/** The wave's pages that came in, as the outputs that publish them, in the wave's order. */ export function aiSiteImportWaveOutputs(state, wave, items, outputs) {
|
|
50
|
+
const rows = new Map(items.map((row)=>[
|
|
51
|
+
row.slot,
|
|
52
|
+
row
|
|
53
|
+
]));
|
|
54
|
+
return wave.slots.flatMap((slot)=>{
|
|
55
|
+
const page = state.pages.find((entry)=>entry.slot === slot);
|
|
56
|
+
if (!page || !delivered(rows.get(slot))) return [];
|
|
57
|
+
const output = outputs.find((entry)=>entry.resource === 'screen' && entry.id === page.draftId);
|
|
58
|
+
return output ? [
|
|
59
|
+
output
|
|
60
|
+
] : [];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A closed wave's pages, live in the site's preview (AGL-4084). `null` when
|
|
65
|
+
* nothing went live: the site is not in preview, or none of the wave's pages
|
|
66
|
+
* came in. The job's earlier publish is carried in the answer.
|
|
67
|
+
*/ export async function aiPublishImportedWave(firestore, input, deps = {}) {
|
|
68
|
+
var _deps_inPreview, _input_job_sitePublish, _deps_publish;
|
|
69
|
+
const hostId = input.job.hostId;
|
|
70
|
+
if (!hostId) return null;
|
|
71
|
+
const pages = aiSiteImportWaveOutputs(input.state, input.wave, input.items, input.outputs);
|
|
72
|
+
if (!pages.length) return null;
|
|
73
|
+
if (!await ((_deps_inPreview = deps.inPreview) != null ? _deps_inPreview : readInPreview)(firestore, hostId)) return null;
|
|
74
|
+
const publishDeps = {
|
|
75
|
+
// Published when its switch makes it public (AGL-4086), never when a wave lands.
|
|
76
|
+
sendSitePublished: async ()=>undefined
|
|
77
|
+
};
|
|
78
|
+
const already = (_input_job_sitePublish = input.job.sitePublish) != null ? _input_job_sitePublish : null;
|
|
79
|
+
const result = await ((_deps_publish = deps.publish) != null ? _deps_publish : aiPublishGuidedSite)(firestore, _extends({
|
|
80
|
+
job: input.job,
|
|
81
|
+
outputs: pages,
|
|
82
|
+
now: input.now,
|
|
83
|
+
// The links to a page still to come, or one that did not come in, come out.
|
|
84
|
+
unbuiltScreenIds: aiSiteImportUnbuilt(input.state, input.items),
|
|
85
|
+
siteOrder: input.state.pages.map((page)=>page.draftId)
|
|
86
|
+
}, (already == null ? void 0 : already.published.length) ? {
|
|
87
|
+
retry: true
|
|
88
|
+
} : {}), publishDeps);
|
|
89
|
+
return already ? aiMergeSitePublish(already, result) : result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=ai-site-import-publish.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-site-import-publish.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * EACH WAVE LANDS IN THE SITE'S PREVIEW (AGL-4084).\n *\n * A site brought in starts in preview (AGL-4086, `host.preview`): the public\n * sees its notice, and its team sees the whole site. So when a wave closes,\n * its pages go live INTO THE PREVIEW through the same writes a guided\n * start's publish makes (`aiPublishGuidedSite`): each page takes its\n * address, Home takes `/`, the links to a page still a draft come out, and\n * the cached pages go. The team opens the site and sees the wave; the public\n * still sees the notice until the person makes the site public.\n *\n * Only into a preview. A site that is not in preview keeps the wave's pages\n * as drafts, as every import did before: the public never sees a page no\n * person has looked at. And `site_published` is not sent here: the site is\n * published when its switch makes it public (AGL-4086), not when a wave\n * lands.\n *=========================================*/\n\nimport { isHostInPreview } from '@aglyn/aglyn/app-utils/site-preview'\nimport type { AiJob, AiJobItemLedger, AiJobOutput, AiJobSitePublish } from '../model/ai-jobs.types'\nimport type { AiSiteImportState, AiSiteImportWave } from '../model/ai-site-import'\nimport { aiMergeSitePublish, aiPublishGuidedSite, type AiSitePublishDeps } from './ai-site-publish'\n\nexport interface AiSiteImportPublishDeps {\n publish?: typeof aiPublishGuidedSite\n /** Whether the site is in preview now. */\n inPreview?: (firestore: FirebaseFirestore.Firestore, hostId: string) => Promise<boolean>\n}\n\nasync function readInPreview(firestore: FirebaseFirestore.Firestore, hostId: string): Promise<boolean> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n return host.exists && isHostInPreview({ preview: host.get('preview') })\n}\n\nconst delivered = (row: AiJobItemLedger | undefined) => row?.status === 'succeeded' || row?.status === 'degraded'\n\n/** The pages of the import a link may not go to yet: every one not in, the wave's own failures included. */\nexport function aiSiteImportUnbuilt(state: AiSiteImportState, items: readonly AiJobItemLedger[]): string[] {\n const rows = new Map(items.map((row) => [row.slot, row]))\n return state.pages.filter((page) => !delivered(rows.get(page.slot))).map((page) => page.draftId)\n}\n\n/** The wave's pages that came in, as the outputs that publish them, in the wave's order. */\nexport function aiSiteImportWaveOutputs(\n state: AiSiteImportState,\n wave: AiSiteImportWave,\n items: readonly AiJobItemLedger[],\n outputs: readonly AiJobOutput[],\n): AiJobOutput[] {\n const rows = new Map(items.map((row) => [row.slot, row]))\n return wave.slots.flatMap((slot) => {\n const page = state.pages.find((entry) => entry.slot === slot)\n if (!page || !delivered(rows.get(slot))) return []\n const output = outputs.find((entry) => entry.resource === 'screen' && entry.id === page.draftId)\n return output ? [output] : []\n })\n}\n\n/**\n * A closed wave's pages, live in the site's preview (AGL-4084). `null` when\n * nothing went live: the site is not in preview, or none of the wave's pages\n * came in. The job's earlier publish is carried in the answer.\n */\nexport async function aiPublishImportedWave(\n firestore: FirebaseFirestore.Firestore,\n input: {\n job: Pick<AiJob, '$id' | 'orgId' | 'hostId'> & Partial<Pick<AiJob, 'inputs' | 'sitePublish'>>\n state: AiSiteImportState\n wave: AiSiteImportWave\n items: readonly AiJobItemLedger[]\n outputs: readonly AiJobOutput[]\n now: Date\n },\n deps: AiSiteImportPublishDeps = {},\n): Promise<AiJobSitePublish | null> {\n const hostId = input.job.hostId\n if (!hostId) return null\n const pages = aiSiteImportWaveOutputs(input.state, input.wave, input.items, input.outputs)\n if (!pages.length) return null\n if (!(await (deps.inPreview ?? readInPreview)(firestore, hostId))) return null\n const publishDeps: AiSitePublishDeps = {\n // Published when its switch makes it public (AGL-4086), never when a wave lands.\n sendSitePublished: async () => undefined,\n }\n const already = input.job.sitePublish ?? null\n const result = await (deps.publish ?? aiPublishGuidedSite)(\n firestore,\n {\n job: input.job,\n outputs: pages,\n now: input.now,\n // The links to a page still to come, or one that did not come in, come out.\n unbuiltScreenIds: aiSiteImportUnbuilt(input.state, input.items),\n siteOrder: input.state.pages.map((page) => page.draftId),\n // A later wave joins what is live: it never takes the home address again.\n ...(already?.published.length ? { retry: true } : {}),\n },\n publishDeps,\n )\n return already ? aiMergeSitePublish(already, result) : result\n}\n"],"names":["isHostInPreview","aiMergeSitePublish","aiPublishGuidedSite","readInPreview","firestore","hostId","host","collection","doc","get","exists","preview","delivered","row","status","aiSiteImportUnbuilt","state","items","rows","Map","map","slot","pages","filter","page","draftId","aiSiteImportWaveOutputs","wave","outputs","slots","flatMap","find","entry","output","resource","id","aiPublishImportedWave","input","deps","job","length","inPreview","publishDeps","sendSitePublished","undefined","already","sitePublish","result","publish","now","unbuiltScreenIds","siteOrder","published","retry"],"mappings":"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"}
|
|
@@ -136,7 +136,16 @@ export interface PublicFetchResult {
|
|
|
136
136
|
location?: string;
|
|
137
137
|
/** The body's bytes, with `binary`; absent otherwise. */
|
|
138
138
|
data?: Uint8Array;
|
|
139
|
+
/**
|
|
140
|
+
* Whose edge refused an error answer, when its headers say (AGL-4083): the
|
|
141
|
+
* bot protection a site owner opens for AglynBot is that edge's setting.
|
|
142
|
+
*/
|
|
143
|
+
edge?: PublicFetchEdge;
|
|
139
144
|
}
|
|
145
|
+
/** An edge network that names itself on the answers it refuses with. */
|
|
146
|
+
export type PublicFetchEdge = 'vercel' | 'cloudflare';
|
|
147
|
+
/** The edge an answer came through, from the headers each one sets; `null` for neither. */
|
|
148
|
+
export declare function publicFetchEdgeOf(headers: Pick<Headers, 'get'>): PublicFetchEdge | null;
|
|
140
149
|
export interface PublicFetchOptions {
|
|
141
150
|
maxBytes: number;
|
|
142
151
|
accept: RegExp;
|
|
@@ -146,6 +155,14 @@ export interface PublicFetchOptions {
|
|
|
146
155
|
userAgent?: string;
|
|
147
156
|
/** Sent as `Accept`. */
|
|
148
157
|
acceptHeader?: string;
|
|
158
|
+
/**
|
|
159
|
+
* More request headers for one hop, asked again for EVERY hop's own address
|
|
160
|
+
* (AGL-4083, AGL-4085), so a header meant for one host never rides a
|
|
161
|
+
* redirect to another. Sent under `Accept` and `User-Agent`, and neither
|
|
162
|
+
* can be changed through here. The site import's edge header goes this
|
|
163
|
+
* way, and only to the hosts `siteImportEdgeHeadersFor` decided are ours.
|
|
164
|
+
*/
|
|
165
|
+
headersFor?: (url: URL) => Readonly<Record<string, string>> | undefined;
|
|
149
166
|
/** Asked of every redirect hop before it is followed; the SSRF checks run as well. */
|
|
150
167
|
redirectAllowed?: (from: URL, to: URL) => boolean;
|
|
151
168
|
/** Refuse a body over `maxBytes` rather than cut it. */
|
|
@@ -169,6 +186,7 @@ export declare function fetchPublicText(url: string, options: {
|
|
|
169
186
|
accept: RegExp;
|
|
170
187
|
signal?: AbortSignal;
|
|
171
188
|
timeoutMs?: number;
|
|
189
|
+
headersFor?: PublicFetchOptions['headersFor'];
|
|
172
190
|
}): Promise<{
|
|
173
191
|
text: string;
|
|
174
192
|
url: string;
|
|
@@ -185,8 +203,15 @@ export interface AiReferencePage {
|
|
|
185
203
|
html: string;
|
|
186
204
|
stylesheets: string[];
|
|
187
205
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
206
|
+
/**
|
|
207
|
+
* A page and its first stylesheets through {@link fetchPublicText}; `null` when the page cannot be read.
|
|
208
|
+
* `headersFor` is asked for every request and every redirect hop, the page's
|
|
209
|
+
* and each stylesheet's, by its own address (AGL-4083, AGL-4085): the site
|
|
210
|
+
* import's edge header, built by `siteImportEdgeHeadersFor`, answers only for
|
|
211
|
+
* the site's own origins we serve, so a hop or a stylesheet anywhere else (a
|
|
212
|
+
* CDN's, another site's) carries nothing.
|
|
213
|
+
*/
|
|
214
|
+
export declare function readReferencePage(url: URL, signal: AbortSignal, headersFor?: PublicFetchOptions['headersFor']): Promise<AiReferencePage | null>;
|
|
190
215
|
/** A read page's brand colors: every color its markup and stylesheets use, ranked. */
|
|
191
216
|
export declare function referencePageColors(page: Pick<AiReferencePage, 'html' | 'stylesheets'>): string[];
|
|
192
217
|
/** The two reads that leave the process, replaceable where a spec needs to. */
|
|
@@ -298,6 +298,13 @@ const HREF_ATTRIBUTE = /\bhref\s*=\s*["']?\s*([^"'\s>]+)/i;
|
|
|
298
298
|
truncated
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
|
+
/** The edge an answer came through, from the headers each one sets; `null` for neither. */ export function publicFetchEdgeOf(headers) {
|
|
302
|
+
var _headers_get;
|
|
303
|
+
const server = ((_headers_get = headers.get('server')) != null ? _headers_get : '').toLowerCase();
|
|
304
|
+
if (headers.get('x-vercel-mitigated') || headers.get('x-vercel-id') || server === 'vercel') return 'vercel';
|
|
305
|
+
if (headers.get('cf-mitigated') || headers.get('cf-ray') || server === 'cloudflare') return 'cloudflare';
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
301
308
|
function refusedFetch(refusal, url, extra = {}) {
|
|
302
309
|
return _extends({
|
|
303
310
|
ok: false,
|
|
@@ -309,6 +316,14 @@ function refusedFetch(refusal, url, extra = {}) {
|
|
|
309
316
|
refusal
|
|
310
317
|
}, extra);
|
|
311
318
|
}
|
|
319
|
+
/** The caller's extra headers, without any that would say what the request is: those are the fetch's own. */ function publicFetchExtraHeaders(headers) {
|
|
320
|
+
const out = {};
|
|
321
|
+
for (const [name, value] of Object.entries(headers != null ? headers : {})){
|
|
322
|
+
if (/^(?:accept|user-agent|host|cookie|authorization|content-length)$/i.test(name)) continue;
|
|
323
|
+
if (typeof value === 'string') out[name] = value;
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
312
327
|
/**
|
|
313
328
|
* A public https resource as text, through the plugin fetch proxy's SSRF
|
|
314
329
|
* guard: the host must resolve only to public addresses and the socket is
|
|
@@ -347,7 +362,7 @@ function refusedFetch(refusal, url, extra = {}) {
|
|
|
347
362
|
redirect: 'manual',
|
|
348
363
|
signal: clock.signal,
|
|
349
364
|
dispatcher,
|
|
350
|
-
headers: _extends({
|
|
365
|
+
headers: _extends({}, publicFetchExtraHeaders(options.headersFor == null ? void 0 : options.headersFor.call(options, parsed)), {
|
|
351
366
|
Accept: (_options_acceptHeader = options.acceptHeader) != null ? _options_acceptHeader : 'text/html, text/css;q=0.9'
|
|
352
367
|
}, options.userAgent ? {
|
|
353
368
|
'User-Agent': options.userAgent
|
|
@@ -382,9 +397,12 @@ function refusedFetch(refusal, url, extra = {}) {
|
|
|
382
397
|
if (!response.ok || !response.body) {
|
|
383
398
|
var _response_body1;
|
|
384
399
|
await ((_response_body1 = response.body) == null ? void 0 : _response_body1.cancel().catch(()=>undefined));
|
|
385
|
-
|
|
400
|
+
const edge = publicFetchEdgeOf(response.headers);
|
|
401
|
+
return refusedFetch('http-status', parsed.toString(), _extends({
|
|
386
402
|
status: response.status
|
|
387
|
-
}
|
|
403
|
+
}, edge ? {
|
|
404
|
+
edge
|
|
405
|
+
} : {}));
|
|
388
406
|
}
|
|
389
407
|
const contentType = (_response_headers_get = response.headers.get('content-type')) != null ? _response_headers_get : '';
|
|
390
408
|
if (!options.accept.test(contentType)) {
|
|
@@ -428,20 +446,31 @@ function refusedFetch(refusal, url, extra = {}) {
|
|
|
428
446
|
url: result.url
|
|
429
447
|
} : null;
|
|
430
448
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
449
|
+
/**
|
|
450
|
+
* A page and its first stylesheets through {@link fetchPublicText}; `null` when the page cannot be read.
|
|
451
|
+
* `headersFor` is asked for every request and every redirect hop, the page's
|
|
452
|
+
* and each stylesheet's, by its own address (AGL-4083, AGL-4085): the site
|
|
453
|
+
* import's edge header, built by `siteImportEdgeHeadersFor`, answers only for
|
|
454
|
+
* the site's own origins we serve, so a hop or a stylesheet anywhere else (a
|
|
455
|
+
* CDN's, another site's) carries nothing.
|
|
456
|
+
*/ export async function readReferencePage(url, signal, headersFor) {
|
|
457
|
+
const page = await fetchPublicText(url.toString(), _extends({
|
|
433
458
|
maxBytes: AI_THEME_REFERENCE_MAX_BYTES,
|
|
434
459
|
accept: /text\/html|application\/xhtml\+xml/i,
|
|
435
460
|
signal
|
|
436
|
-
}
|
|
461
|
+
}, headersFor ? {
|
|
462
|
+
headersFor
|
|
463
|
+
} : {}));
|
|
437
464
|
if (!page) return null;
|
|
438
465
|
const stylesheets = [];
|
|
439
466
|
for (const sheet of stylesheetUrlsOf(page.text, page.url)){
|
|
440
|
-
const css = await fetchPublicText(sheet, {
|
|
467
|
+
const css = await fetchPublicText(sheet, _extends({
|
|
441
468
|
maxBytes: AI_THEME_STYLESHEET_MAX_BYTES,
|
|
442
469
|
accept: /text\/css/i,
|
|
443
470
|
signal
|
|
444
|
-
}
|
|
471
|
+
}, headersFor ? {
|
|
472
|
+
headersFor
|
|
473
|
+
} : {}));
|
|
445
474
|
if (css) stylesheets.push(css.text);
|
|
446
475
|
}
|
|
447
476
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-theme-brand-inputs.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n AglynOrgBilling,\n OrgBrandingProfile,\n} from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { normalizeThemeHex } from '../tools/ai-theme-tool'\nimport { AI_THEME_BRAND_BUDGET_MS } from './ai-job-theme-budget'\nimport {\n aiMediaAssetLocation,\n loadAiMediaSharp,\n readAiMediaAssetBytes,\n type AiMediaAssetLocation,\n} from './ai-media-asset'\n\n/**\n * The brand a theme job builds on (AGL-2938): the colors that belong to the\n * site before anyone describes them.\n *\n * - **The workspace's brand color** — a white-label workspace's, and only\n * theirs: without the entitlement the resolved profile is the platform's,\n * which is no customer's brand.\n * - **The site logo** — when the logo is an asset of the site's own media\n * library or its org's, read from storage and reduced to its dominant\n * colors. Never fetched from a URL: a logo that is not a library asset is\n * left alone.\n * - **A page the brief links to** — fetched through the same SSRF guard the\n * plugin fetch proxy uses (a public address, pinned; https only; every\n * redirect re-validated), capped in bytes and time, and reduced to the\n * colors its markup and stylesheets use.\n *\n * Only hex colors leave this module. Nothing a page or a logo SAYS reaches a\n * prompt, so there is nothing in them to follow. Each source is best effort:\n * one that cannot be read is named in a note and the proposal is made\n * without it.\n */\n\nexport type AiThemeBrandSource = 'organization' | 'logo' | 'reference'\n\n/** How the prompt names where each brand color came from. */\nexport const AI_THEME_BRAND_SOURCE_WORDS: Record<AiThemeBrandSource, string> = {\n organization: 'the workspace brand color',\n logo: 'from the site logo',\n reference: 'from the page the brief links to',\n}\n\nexport interface AiThemeBrandColor {\n hex: string\n source: AiThemeBrandSource\n}\n\nexport interface AiThemeBrandInputs {\n colors: AiThemeBrandColor[]\n /** A source that could not be read, in customer-safe words. */\n notes: string[]\n}\n\n/** The most colors one source contributes. */\nexport const AI_THEME_BRAND_COLORS_PER_SOURCE = 4\n\n/** The largest logo decoded for its colors. */\nexport const AI_THEME_LOGO_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most of a linked page, and of each of its stylesheets, that is read. */\nexport const AI_THEME_REFERENCE_MAX_BYTES = 512 * 1024\nexport const AI_THEME_STYLESHEET_MAX_BYTES = 256 * 1024\n\n/** The stylesheets of a linked page that are read beside it. */\nexport const AI_THEME_REFERENCE_STYLESHEETS = 2\n\n/**\n * The wall clock brand colors may take, declared with the theme step's time\n * (`ai-job-theme-budget.ts`), which counts it (AGL-3035).\n */\nexport { AI_THEME_BRAND_BUDGET_MS }\n\nconst FETCH_TIMEOUT_MS = 4_000\nconst MAX_REDIRECT_HOPS = 3\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])\n\ntype Rgb = [red: number, green: number, blue: number]\n\nconst toHex = ([red, green, blue]: Rgb) =>\n `#${[red, green, blue]\n .map((channel) => Math.round(Math.min(255, Math.max(0, channel))).toString(16).padStart(2, '0'))\n .join('')}`\n\nconst channelsOf = (hex: string): Rgb => [\n parseInt(hex.slice(1, 3), 16),\n parseInt(hex.slice(3, 5), 16),\n parseInt(hex.slice(5, 7), 16),\n]\n\nconst distance = (a: Rgb, b: Rgb) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])\n\n/** How far a color is from grey: the spread of its channels. */\nconst chroma = ([red, green, blue]: Rgb) => Math.max(red, green, blue) - Math.min(red, green, blue)\n\n/** Colors closer than this read as the same color in a palette. */\nconst SAME_COLOR_DISTANCE = 40\n\n/** A spread under this reads as a grey, which says little about a brand. */\nconst GREY_CHROMA = 38\n\n/** The distinct colors of a ranked list, colorful ones first when there are any. */\nfunction distinctColors(ranked: readonly Rgb[], count: number): string[] {\n const colorful = ranked.filter((color) => chroma(color) > GREY_CHROMA)\n const chosen: Rgb[] = []\n for (const color of colorful.length ? colorful : ranked) {\n if (chosen.some((other) => distance(other, color) < SAME_COLOR_DISTANCE)) continue\n chosen.push(color)\n if (chosen.length === count) break\n }\n return chosen.map(toHex)\n}\n\n/* ------------------------------------------------------------------------ *\n * The workspace's brand color\n * ------------------------------------------------------------------------ */\n\n/** A white-label workspace's own brand color; nothing for any other workspace. */\nexport function organizationBrandColors(\n org: Partial<AglynOrgBilling> | null | undefined,\n): AiThemeBrandColor[] {\n if (!checkEntitlement(org, 'whiteLabel')) return []\n const raw = (org?.brandingProfile as OrgBrandingProfile | undefined)?.primaryColor\n const hex = typeof raw === 'string' ? normalizeThemeHex(raw) : null\n return hex ? [{ hex, source: 'organization' }] : []\n}\n\n/* ------------------------------------------------------------------------ *\n * The site logo\n * ------------------------------------------------------------------------ */\n\nexport type AiThemeLogoLocation = AiMediaAssetLocation\n\n/**\n * Where a site's logo lives in a media library: a media reference or a CDN\n * path naming this site's library, or its org's. Anything else — an external\n * URL, an asset of some other site or org — has no location, and the logo is\n * left alone. The media library's own rule, shared with every job that reads\n * an asset (`ai-media-asset.ts`).\n */\nexport const logoMediaLocation = aiMediaAssetLocation\n\n/**\n * The dominant colors of an image's pixels: transparent pixels skipped, the\n * rest grouped into coarse buckets, the busiest distinct buckets first, and\n * the near-white and near-black edges a logo sits on dropped while anything\n * else is there.\n */\nexport function dominantColorsFromPixels(\n data: ArrayLike<number>,\n channels: number,\n count = AI_THEME_BRAND_COLORS_PER_SOURCE,\n): string[] {\n const buckets = new Map<number, { sum: Rgb; pixels: number }>()\n for (let index = 0; index + channels <= data.length; index += channels) {\n if (channels >= 4 && data[index + 3] < 128) continue\n const pixel: Rgb = [data[index], data[index + 1], data[index + 2]]\n const key = ((pixel[0] >> 4) << 8) | ((pixel[1] >> 4) << 4) | (pixel[2] >> 4)\n const bucket = buckets.get(key) ?? { sum: [0, 0, 0] as Rgb, pixels: 0 }\n bucket.sum = [bucket.sum[0] + pixel[0], bucket.sum[1] + pixel[1], bucket.sum[2] + pixel[2]]\n bucket.pixels += 1\n buckets.set(key, bucket)\n }\n const ranked = [...buckets.values()]\n .sort((a, b) => b.pixels - a.pixels)\n .map(({ sum, pixels }): Rgb => [sum[0] / pixels, sum[1] / pixels, sum[2] / pixels])\n const edge = (color: Rgb) => Math.min(...color) > 235 || Math.max(...color) < 20\n const inside = ranked.filter((color) => !edge(color))\n return distinctColors(inside.length ? inside : ranked, count)\n}\n\n/** The part of `sharp` the logo read uses. */\ninterface SharpPixelPipeline {\n resize(width: number, height: number, options: { fit: 'inside' }): SharpPixelPipeline\n ensureAlpha(): SharpPixelPipeline\n raw(): SharpPixelPipeline\n toBuffer(options: { resolveWithObject: true }): Promise<{\n data: Buffer\n info: { channels: number }\n }>\n}\ntype SharpPixels = (input: Buffer, options?: { limitInputPixels?: number }) => SharpPixelPipeline\n\nconst LOGO_TYPES = /^image\\/(png|jpeg|webp|gif|avif|svg\\+xml)$/\n\n/**\n * A library logo's colors, read the way the media CDN would serve it\n * (`readAiMediaAssetBytes`). `null` when the asset may not be read or the bytes\n * cannot be decoded.\n *\n * On Vercel the read and the decode run in the plugins' image function, the\n * one function that ships `sharp`'s native library (`ai-image-reads.ts`,\n * AGL-4048), inside the brand reads' own bound (`signal`); anywhere else they\n * run here. Either way an answer that does not come is a logo whose colors\n * could not be read.\n */\nasync function readLogoColors(\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n signal?: AbortSignal,\n): Promise<string[] | null> {\n const images = await import('./ai-image-reads')\n const origin = images.aiImageReadsOrigin()\n if (!origin) return readAiThemeLogoColorsAt(firestore, location, hostId)\n // The location was held to the site's org already: an org asset's scope\n // IS that org, and a site's own asset needs none.\n const orgId = location.scope.isOrg ? location.scope.scopeId : ''\n const payload = images.aiImageReadsRequest('logo-colors', location, hostId, orgId)\n if (!payload) return null\n const answer = await images.requestAiImageReads<{ colors?: unknown }>(\n origin,\n payload,\n signal ?? AbortSignal.timeout(AI_THEME_BRAND_BUDGET_MS),\n )\n return logoColorsAnswer(answer?.colors)\n}\n\n/**\n * A library logo's colors read and decoded in THIS process, from a location\n * already held to the media library's rule: what `readLogoColors` does off\n * Vercel, and what the images function does for it on Vercel.\n */\nexport async function readAiThemeLogoColorsAt(\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n): Promise<string[] | null> {\n const asset = await readAiMediaAssetBytes(firestore, location, hostId, {\n maxBytes: AI_THEME_LOGO_MAX_BYTES,\n types: LOGO_TYPES,\n })\n if (!asset) return null\n const sharp = (await loadAiMediaSharp()) as SharpPixels\n const { data, info } = await sharp(asset.buffer, { limitInputPixels: 64_000_000 })\n .resize(48, 48, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n return dominantColorsFromPixels(data, info.channels)\n}\n\n/** What the images function answered, as hex colors: only colors leave it. */\nfunction logoColorsAnswer(colors: unknown): string[] | null {\n if (!Array.isArray(colors)) return null\n return colors\n .map((color) => (typeof color === 'string' ? normalizeThemeHex(color) : null))\n .filter((hex): hex is string => Boolean(hex))\n .slice(0, AI_THEME_BRAND_COLORS_PER_SOURCE)\n}\n\n/* ------------------------------------------------------------------------ *\n * A page the brief links to\n * ------------------------------------------------------------------------ */\n\nconst URL_IN_BRIEF = /\\bhttps?:\\/\\/[^\\s<>\"'`)\\]]+/i\n\n/** The first https link in a brief, or `null` — an http link is not followed. */\nexport function referenceUrlOf(brief: string): URL | null {\n const match = URL_IN_BRIEF.exec(brief)\n if (!match) return null\n try {\n const url = new URL(match[0].replace(/[.,;:!?]+$/, ''))\n return url.protocol === 'https:' ? url : null\n } catch {\n return null\n }\n}\n\n/** How much a page's declared `theme-color` counts beside one use of a color. */\nexport const AI_THEME_THEME_COLOR_WEIGHT = 25\n\nconst THEME_COLOR_META = /<meta\\b[^>]*\\bname\\s*=\\s*[\"']?theme-color\\b[^>]*>/gi\nconst CONTENT_ATTRIBUTE = /\\bcontent\\s*=\\s*[\"']?\\s*([^\"'\\s>]+)/i\n// A `#` after `&` is a character reference (`'`), not a color.\nconst HEX_LITERAL = /(?<!&)#(?:[0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-z])/gi\nconst RGB_LITERAL = /rgba?\\(\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})/gi\n\n/** Every color a page's markup or a stylesheet uses, weighted by how often. */\nexport function colorsFromMarkup(text: string): Map<string, number> {\n const weights = new Map<string, number>()\n const add = (hex: string | null, weight: number) => {\n if (hex) weights.set(hex, (weights.get(hex) ?? 0) + weight)\n }\n for (const tag of text.match(THEME_COLOR_META) ?? []) {\n add(normalizeThemeHex(CONTENT_ATTRIBUTE.exec(tag)?.[1] ?? ''), AI_THEME_THEME_COLOR_WEIGHT)\n }\n for (const match of text.matchAll(HEX_LITERAL)) add(normalizeThemeHex(match[0]), 1)\n for (const match of text.matchAll(RGB_LITERAL)) {\n add(toHex([Number(match[1]), Number(match[2]), Number(match[3])]), 1)\n }\n return weights\n}\n\nconst STYLESHEET_LINK = /<link\\b[^>]*\\brel\\s*=\\s*[\"']?stylesheet\\b[^>]*>/gi\nconst HREF_ATTRIBUTE = /\\bhref\\s*=\\s*[\"']?\\s*([^\"'\\s>]+)/i\n\n/** The https stylesheets a page links, resolved against it, at most a few. */\nexport function stylesheetUrlsOf(html: string, pageUrl: string): string[] {\n const urls: string[] = []\n for (const tag of html.match(STYLESHEET_LINK) ?? []) {\n const href = HREF_ATTRIBUTE.exec(tag)?.[1]\n if (!href) continue\n try {\n const url = new URL(href, pageUrl)\n if (url.protocol === 'https:' && !urls.includes(url.toString())) urls.push(url.toString())\n } catch {\n continue\n }\n if (urls.length === AI_THEME_REFERENCE_STYLESHEETS) break\n }\n return urls\n}\n\n/** The most used distinct colors, the colorful before the greys. */\nexport function rankBrandColors(\n weights: ReadonlyMap<string, number>,\n count = AI_THEME_BRAND_COLORS_PER_SOURCE,\n): string[] {\n const ranked = [...weights.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([hex]) => channelsOf(hex))\n return distinctColors(ranked, count)\n}\n\n/** One abort for the caller's signal and a clock, whichever ends first. */\nfunction deadline(signal: AbortSignal | undefined, ms: number) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), ms)\n const onAbort = () => controller.abort()\n if (signal?.aborted) controller.abort()\n signal?.addEventListener('abort', onAbort)\n return {\n signal: controller.signal,\n done: () => {\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n },\n }\n}\n\n/**\n * The body as text, to `maxBytes`. Past the cap it is cut there, or with\n * `refuseOver` it is not read at all: `null`, the read canceled.\n */\nasync function readCapped(\n body: ReadableStream<Uint8Array>,\n maxBytes: number,\n refuseOver = false,\n binary = false,\n): Promise<{ text: string; bytes: number; truncated: boolean; data?: Uint8Array } | null> {\n const reader = body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n let truncated = false\n for (;;) {\n const { done, value } = await reader.read()\n if (done || !value) break\n const room = maxBytes - total\n if (value.byteLength > room) {\n await reader.cancel().catch(() => undefined)\n if (refuseOver) return null\n chunks.push(value.subarray(0, room))\n total += room\n truncated = true\n break\n }\n chunks.push(value)\n total += value.byteLength\n }\n const joined = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n joined.set(chunk, offset)\n offset += chunk.byteLength\n }\n // A picture's bytes are kept as they came (AGL-3976), never decoded as text.\n if (binary) return { text: '', bytes: total, truncated, data: joined }\n return { text: new TextDecoder().decode(joined), bytes: total, truncated }\n}\n\n/** Why {@link fetchPublicTextDetailed} read nothing. */\nexport type PublicFetchRefusal =\n /** Not https, a port, credentials, or not a URL at all. */\n | 'refused-url'\n /** The name resolves to a private address, or not at all. */\n | 'private-address'\n /** A redirect the caller's `redirectAllowed` refused. */\n | 'redirect-refused'\n | 'too-many-redirects'\n /** An answer that was not 2xx; `status` says which. */\n | 'http-status'\n /** A 2xx whose content type the caller does not accept. */\n | 'content-type'\n /** Past `maxBytes` with `refuseOver` set. */\n | 'too-large'\n /** The socket, the clock or the caller's signal ended it. */\n | 'network'\n\n/**\n * One shape for both outcomes, read by `ok`: a union does not narrow across\n * this repo's library boundaries (`strictNullChecks` is off), so a caller\n * could not reach either arm's fields.\n */\nexport interface PublicFetchResult {\n ok: boolean\n /** The address read, after redirects; or the one that was refused. */\n url: string\n /** Empty unless `ok`. */\n text: string\n bytes: number\n /** Cut at `maxBytes` (never with `refuseOver`). */\n truncated: boolean\n contentType: string\n /** Null when `ok`. */\n refusal: PublicFetchRefusal | null\n status?: number\n /** Where a refused redirect pointed. */\n location?: string\n /** The body's bytes, with `binary`; absent otherwise. */\n data?: Uint8Array\n}\n\nfunction refusedFetch(\n refusal: PublicFetchRefusal,\n url: string,\n extra: { status?: number; location?: string } = {},\n): PublicFetchResult {\n return { ok: false, url, text: '', bytes: 0, truncated: false, contentType: '', refusal, ...extra }\n}\n\nexport interface PublicFetchOptions {\n maxBytes: number\n accept: RegExp\n signal?: AbortSignal\n timeoutMs?: number\n /** Sent as `User-Agent`; the runtime's own when absent. */\n userAgent?: string\n /** Sent as `Accept`. */\n acceptHeader?: string\n /** Asked of every redirect hop before it is followed; the SSRF checks run as well. */\n redirectAllowed?: (from: URL, to: URL) => boolean\n /** Refuse a body over `maxBytes` rather than cut it. */\n refuseOver?: boolean\n /** Keep the body's bytes as `data` rather than decode them as text (a picture, AGL-3976). */\n binary?: boolean\n}\n\n/**\n * A public https resource as text, through the plugin fetch proxy's SSRF\n * guard: the host must resolve only to public addresses and the socket is\n * pinned to the address checked; no credentials in the URL and no port but\n * the default; every redirect re-enters the same checks and the caller's own\n * `redirectAllowed`; the body is read to a byte cap under a clock, and only a\n * content type the caller accepts is read at all. Says why when it reads\n * nothing.\n */\nexport async function fetchPublicTextDetailed(\n url: string,\n options: PublicFetchOptions,\n): Promise<PublicFetchResult> {\n const { createPinnedDispatcher, resolvePublicIp } = await import('./ai-theme-brand-server')\n let current = url\n for (let hop = 0; hop <= MAX_REDIRECT_HOPS; hop += 1) {\n let parsed: URL\n try {\n parsed = new URL(current)\n } catch {\n return refusedFetch('refused-url', current)\n }\n if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {\n return refusedFetch('refused-url', current)\n }\n if (parsed.port && parsed.port !== '443') {\n return refusedFetch('refused-url', current)\n }\n const pinned = await resolvePublicIp(parsed.hostname)\n if (!pinned) return refusedFetch('private-address', parsed.toString())\n const dispatcher = createPinnedDispatcher(pinned)\n const clock = deadline(options.signal, options.timeoutMs ?? FETCH_TIMEOUT_MS)\n try {\n // `dispatcher` is undici's, which the global fetch honors; a variable\n // rather than a literal keeps the DOM `RequestInit` check off it.\n const init = {\n method: 'GET',\n redirect: 'manual' as const,\n signal: clock.signal,\n dispatcher,\n headers: {\n Accept: options.acceptHeader ?? 'text/html, text/css;q=0.9',\n ...(options.userAgent ? { 'User-Agent': options.userAgent } : {}),\n },\n }\n const response = await fetch(parsed.toString(), init as RequestInit)\n if (REDIRECT_STATUSES.has(response.status)) {\n await response.body?.cancel().catch(() => undefined)\n const location = response.headers.get('location')\n if (!location) {\n return refusedFetch('http-status', parsed.toString(), { status: response.status })\n }\n let next: URL\n try {\n next = new URL(location, parsed)\n } catch {\n return refusedFetch('redirect-refused', parsed.toString(), { location })\n }\n if (options.redirectAllowed && !options.redirectAllowed(parsed, next)) {\n return refusedFetch('redirect-refused', parsed.toString(), { location: next.toString() })\n }\n current = next.toString()\n continue\n }\n if (!response.ok || !response.body) {\n await response.body?.cancel().catch(() => undefined)\n return refusedFetch('http-status', parsed.toString(), { status: response.status })\n }\n const contentType = response.headers.get('content-type') ?? ''\n if (!options.accept.test(contentType)) {\n await response.body.cancel().catch(() => undefined)\n return refusedFetch('content-type', parsed.toString(), { status: response.status })\n }\n const declared = Number(response.headers.get('content-length') ?? '')\n if (options.refuseOver && Number.isFinite(declared) && declared > options.maxBytes) {\n await response.body.cancel().catch(() => undefined)\n return refusedFetch('too-large', parsed.toString(), { status: response.status })\n }\n const body = await readCapped(response.body, options.maxBytes, options.refuseOver, options.binary === true)\n if (!body) {\n return refusedFetch('too-large', parsed.toString(), { status: response.status })\n }\n return { ok: true, url: parsed.toString(), contentType, refusal: null, ...body }\n } catch {\n return refusedFetch('network', parsed.toString())\n } finally {\n clock.done()\n await dispatcher.close().catch(() => undefined)\n }\n }\n return refusedFetch('too-many-redirects', current)\n}\n\n/** {@link fetchPublicTextDetailed}, answering `null` for anything it refused. */\nexport async function fetchPublicText(\n url: string,\n options: { maxBytes: number; accept: RegExp; signal?: AbortSignal; timeoutMs?: number },\n): Promise<{ text: string; url: string } | null> {\n const result = await fetchPublicTextDetailed(url, options)\n return result.ok ? { text: result.text, url: result.url } : null\n}\n\n/**\n * A public page as it was read: its markup and the text of its first\n * stylesheets ({@link AI_THEME_REFERENCE_STYLESHEETS}), each under its byte\n * cap. What a theme job takes its colors from and a site start its\n * inspiration signals (AGL-3975); never a crawl, one page and its sheets.\n */\nexport interface AiReferencePage {\n /** The address the markup was finally read at, after redirects. */\n url: string\n html: string\n stylesheets: string[]\n}\n\n/** A page and its first stylesheets through {@link fetchPublicText}; `null` when the page cannot be read. */\nexport async function readReferencePage(url: URL, signal: AbortSignal): Promise<AiReferencePage | null> {\n const page = await fetchPublicText(url.toString(), {\n maxBytes: AI_THEME_REFERENCE_MAX_BYTES,\n accept: /text\\/html|application\\/xhtml\\+xml/i,\n signal,\n })\n if (!page) return null\n const stylesheets: string[] = []\n for (const sheet of stylesheetUrlsOf(page.text, page.url)) {\n const css = await fetchPublicText(sheet, {\n maxBytes: AI_THEME_STYLESHEET_MAX_BYTES,\n accept: /text\\/css/i,\n signal,\n })\n if (css) stylesheets.push(css.text)\n }\n return { url: page.url, html: page.text, stylesheets }\n}\n\n/** A read page's brand colors: every color its markup and stylesheets use, ranked. */\nexport function referencePageColors(page: Pick<AiReferencePage, 'html' | 'stylesheets'>): string[] {\n const weights = colorsFromMarkup(page.html)\n for (const css of page.stylesheets) {\n for (const [hex, weight] of colorsFromMarkup(css)) {\n weights.set(hex, (weights.get(hex) ?? 0) + weight)\n }\n }\n return rankBrandColors(weights)\n}\n\n/** A linked page's colors, from its markup and its first stylesheets. */\nasync function readReferenceColors(url: URL, signal: AbortSignal): Promise<string[] | null> {\n const page = await readReferencePage(url, signal)\n return page ? referencePageColors(page) : null\n}\n\n/* ------------------------------------------------------------------------ *\n * Gathering\n * ------------------------------------------------------------------------ */\n\n/** The two reads that leave the process, replaceable where a spec needs to. */\nexport interface AiThemeBrandSeams {\n logoColors?: (\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n /** The brand reads' bound, which a read made elsewhere waits no longer than. */\n signal?: AbortSignal,\n ) => Promise<string[] | null>\n referenceColors?: (url: URL, signal: AbortSignal) => Promise<string[] | null>\n}\n\nexport async function gatherAiThemeBrandInputs(\n input: {\n firestore: FirebaseFirestore.Firestore\n org: Partial<AglynOrgBilling> | null\n hostId: string\n /** The site's host document. */\n host: Record<string, unknown>\n brief: string\n signal?: AbortSignal\n },\n seams: AiThemeBrandSeams = {},\n): Promise<AiThemeBrandInputs> {\n const colors: AiThemeBrandColor[] = [...organizationBrandColors(input.org)]\n const notes: string[] = []\n const clock = deadline(input.signal, AI_THEME_BRAND_BUDGET_MS)\n const add = (found: readonly string[], source: AiThemeBrandSource) => {\n for (const hex of found.slice(0, AI_THEME_BRAND_COLORS_PER_SOURCE)) {\n if (!colors.some((color) => color.hex === hex)) colors.push({ hex, source })\n }\n }\n try {\n const location = logoMediaLocation(\n input.host['logoUrl'],\n input.hostId,\n String(input.host['orgId'] ?? ''),\n )\n if (location) {\n const found = await (seams.logoColors ?? readLogoColors)(\n input.firestore,\n location,\n input.hostId,\n clock.signal,\n ).catch(() => null)\n if (found) add(found, 'logo')\n else notes.push(\"The site logo's colors could not be read, so the proposal does not use them.\")\n }\n const reference = referenceUrlOf(input.brief)\n if (reference) {\n const found = clock.signal.aborted\n ? null\n : await (seams.referenceColors ?? readReferenceColors)(reference, clock.signal).catch(\n () => null,\n )\n if (found?.length) add(found, 'reference')\n else {\n notes.push(\n `The page at ${reference.hostname} could not be read for its colors, so the proposal does not use them.`,\n )\n }\n }\n } finally {\n clock.done()\n }\n return { colors, notes }\n}\n"],"names":["checkEntitlement","normalizeThemeHex","AI_THEME_BRAND_BUDGET_MS","aiMediaAssetLocation","loadAiMediaSharp","readAiMediaAssetBytes","AI_THEME_BRAND_SOURCE_WORDS","organization","logo","reference","AI_THEME_BRAND_COLORS_PER_SOURCE","AI_THEME_LOGO_MAX_BYTES","AI_THEME_REFERENCE_MAX_BYTES","AI_THEME_STYLESHEET_MAX_BYTES","AI_THEME_REFERENCE_STYLESHEETS","FETCH_TIMEOUT_MS","MAX_REDIRECT_HOPS","REDIRECT_STATUSES","Set","toHex","red","green","blue","map","channel","Math","round","min","max","toString","padStart","join","channelsOf","hex","parseInt","slice","distance","a","b","hypot","chroma","SAME_COLOR_DISTANCE","GREY_CHROMA","distinctColors","ranked","count","colorful","filter","color","chosen","length","some","other","push","organizationBrandColors","org","raw","brandingProfile","primaryColor","source","logoMediaLocation","dominantColorsFromPixels","data","channels","buckets","Map","index","pixel","key","bucket","get","sum","pixels","set","values","sort","edge","inside","LOGO_TYPES","readLogoColors","firestore","location","hostId","signal","images","origin","aiImageReadsOrigin","readAiThemeLogoColorsAt","orgId","scope","isOrg","scopeId","payload","aiImageReadsRequest","answer","requestAiImageReads","AbortSignal","timeout","logoColorsAnswer","colors","asset","maxBytes","types","sharp","info","buffer","limitInputPixels","resize","fit","ensureAlpha","toBuffer","resolveWithObject","Array","isArray","Boolean","URL_IN_BRIEF","referenceUrlOf","brief","match","exec","url","URL","replace","protocol","AI_THEME_THEME_COLOR_WEIGHT","THEME_COLOR_META","CONTENT_ATTRIBUTE","HEX_LITERAL","RGB_LITERAL","colorsFromMarkup","text","weights","add","weight","tag","matchAll","Number","STYLESHEET_LINK","HREF_ATTRIBUTE","stylesheetUrlsOf","html","pageUrl","urls","href","includes","rankBrandColors","entries","deadline","ms","controller","AbortController","timer","setTimeout","abort","onAbort","aborted","addEventListener","done","clearTimeout","removeEventListener","readCapped","body","refuseOver","binary","reader","getReader","chunks","total","truncated","value","read","room","byteLength","cancel","catch","undefined","subarray","joined","Uint8Array","offset","chunk","bytes","TextDecoder","decode","refusedFetch","refusal","extra","ok","contentType","fetchPublicTextDetailed","options","createPinnedDispatcher","resolvePublicIp","current","hop","parsed","username","password","port","pinned","hostname","dispatcher","clock","timeoutMs","response","init","method","redirect","headers","Accept","acceptHeader","userAgent","fetch","has","status","next","redirectAllowed","accept","test","declared","isFinite","close","fetchPublicText","result","readReferencePage","page","stylesheets","sheet","css","referencePageColors","readReferenceColors","gatherAiThemeBrandInputs","input","seams","notes","found","host","String","logoColors","referenceColors"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-theme-brand-inputs.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type {\n AglynOrgBilling,\n OrgBrandingProfile,\n} from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport { normalizeThemeHex } from '../tools/ai-theme-tool'\nimport { AI_THEME_BRAND_BUDGET_MS } from './ai-job-theme-budget'\nimport {\n aiMediaAssetLocation,\n loadAiMediaSharp,\n readAiMediaAssetBytes,\n type AiMediaAssetLocation,\n} from './ai-media-asset'\n\n/**\n * The brand a theme job builds on (AGL-2938): the colors that belong to the\n * site before anyone describes them.\n *\n * - **The workspace's brand color** — a white-label workspace's, and only\n * theirs: without the entitlement the resolved profile is the platform's,\n * which is no customer's brand.\n * - **The site logo** — when the logo is an asset of the site's own media\n * library or its org's, read from storage and reduced to its dominant\n * colors. Never fetched from a URL: a logo that is not a library asset is\n * left alone.\n * - **A page the brief links to** — fetched through the same SSRF guard the\n * plugin fetch proxy uses (a public address, pinned; https only; every\n * redirect re-validated), capped in bytes and time, and reduced to the\n * colors its markup and stylesheets use.\n *\n * Only hex colors leave this module. Nothing a page or a logo SAYS reaches a\n * prompt, so there is nothing in them to follow. Each source is best effort:\n * one that cannot be read is named in a note and the proposal is made\n * without it.\n */\n\nexport type AiThemeBrandSource = 'organization' | 'logo' | 'reference'\n\n/** How the prompt names where each brand color came from. */\nexport const AI_THEME_BRAND_SOURCE_WORDS: Record<AiThemeBrandSource, string> = {\n organization: 'the workspace brand color',\n logo: 'from the site logo',\n reference: 'from the page the brief links to',\n}\n\nexport interface AiThemeBrandColor {\n hex: string\n source: AiThemeBrandSource\n}\n\nexport interface AiThemeBrandInputs {\n colors: AiThemeBrandColor[]\n /** A source that could not be read, in customer-safe words. */\n notes: string[]\n}\n\n/** The most colors one source contributes. */\nexport const AI_THEME_BRAND_COLORS_PER_SOURCE = 4\n\n/** The largest logo decoded for its colors. */\nexport const AI_THEME_LOGO_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most of a linked page, and of each of its stylesheets, that is read. */\nexport const AI_THEME_REFERENCE_MAX_BYTES = 512 * 1024\nexport const AI_THEME_STYLESHEET_MAX_BYTES = 256 * 1024\n\n/** The stylesheets of a linked page that are read beside it. */\nexport const AI_THEME_REFERENCE_STYLESHEETS = 2\n\n/**\n * The wall clock brand colors may take, declared with the theme step's time\n * (`ai-job-theme-budget.ts`), which counts it (AGL-3035).\n */\nexport { AI_THEME_BRAND_BUDGET_MS }\n\nconst FETCH_TIMEOUT_MS = 4_000\nconst MAX_REDIRECT_HOPS = 3\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308])\n\ntype Rgb = [red: number, green: number, blue: number]\n\nconst toHex = ([red, green, blue]: Rgb) =>\n `#${[red, green, blue]\n .map((channel) => Math.round(Math.min(255, Math.max(0, channel))).toString(16).padStart(2, '0'))\n .join('')}`\n\nconst channelsOf = (hex: string): Rgb => [\n parseInt(hex.slice(1, 3), 16),\n parseInt(hex.slice(3, 5), 16),\n parseInt(hex.slice(5, 7), 16),\n]\n\nconst distance = (a: Rgb, b: Rgb) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2])\n\n/** How far a color is from grey: the spread of its channels. */\nconst chroma = ([red, green, blue]: Rgb) => Math.max(red, green, blue) - Math.min(red, green, blue)\n\n/** Colors closer than this read as the same color in a palette. */\nconst SAME_COLOR_DISTANCE = 40\n\n/** A spread under this reads as a grey, which says little about a brand. */\nconst GREY_CHROMA = 38\n\n/** The distinct colors of a ranked list, colorful ones first when there are any. */\nfunction distinctColors(ranked: readonly Rgb[], count: number): string[] {\n const colorful = ranked.filter((color) => chroma(color) > GREY_CHROMA)\n const chosen: Rgb[] = []\n for (const color of colorful.length ? colorful : ranked) {\n if (chosen.some((other) => distance(other, color) < SAME_COLOR_DISTANCE)) continue\n chosen.push(color)\n if (chosen.length === count) break\n }\n return chosen.map(toHex)\n}\n\n/* ------------------------------------------------------------------------ *\n * The workspace's brand color\n * ------------------------------------------------------------------------ */\n\n/** A white-label workspace's own brand color; nothing for any other workspace. */\nexport function organizationBrandColors(\n org: Partial<AglynOrgBilling> | null | undefined,\n): AiThemeBrandColor[] {\n if (!checkEntitlement(org, 'whiteLabel')) return []\n const raw = (org?.brandingProfile as OrgBrandingProfile | undefined)?.primaryColor\n const hex = typeof raw === 'string' ? normalizeThemeHex(raw) : null\n return hex ? [{ hex, source: 'organization' }] : []\n}\n\n/* ------------------------------------------------------------------------ *\n * The site logo\n * ------------------------------------------------------------------------ */\n\nexport type AiThemeLogoLocation = AiMediaAssetLocation\n\n/**\n * Where a site's logo lives in a media library: a media reference or a CDN\n * path naming this site's library, or its org's. Anything else — an external\n * URL, an asset of some other site or org — has no location, and the logo is\n * left alone. The media library's own rule, shared with every job that reads\n * an asset (`ai-media-asset.ts`).\n */\nexport const logoMediaLocation = aiMediaAssetLocation\n\n/**\n * The dominant colors of an image's pixels: transparent pixels skipped, the\n * rest grouped into coarse buckets, the busiest distinct buckets first, and\n * the near-white and near-black edges a logo sits on dropped while anything\n * else is there.\n */\nexport function dominantColorsFromPixels(\n data: ArrayLike<number>,\n channels: number,\n count = AI_THEME_BRAND_COLORS_PER_SOURCE,\n): string[] {\n const buckets = new Map<number, { sum: Rgb; pixels: number }>()\n for (let index = 0; index + channels <= data.length; index += channels) {\n if (channels >= 4 && data[index + 3] < 128) continue\n const pixel: Rgb = [data[index], data[index + 1], data[index + 2]]\n const key = ((pixel[0] >> 4) << 8) | ((pixel[1] >> 4) << 4) | (pixel[2] >> 4)\n const bucket = buckets.get(key) ?? { sum: [0, 0, 0] as Rgb, pixels: 0 }\n bucket.sum = [bucket.sum[0] + pixel[0], bucket.sum[1] + pixel[1], bucket.sum[2] + pixel[2]]\n bucket.pixels += 1\n buckets.set(key, bucket)\n }\n const ranked = [...buckets.values()]\n .sort((a, b) => b.pixels - a.pixels)\n .map(({ sum, pixels }): Rgb => [sum[0] / pixels, sum[1] / pixels, sum[2] / pixels])\n const edge = (color: Rgb) => Math.min(...color) > 235 || Math.max(...color) < 20\n const inside = ranked.filter((color) => !edge(color))\n return distinctColors(inside.length ? inside : ranked, count)\n}\n\n/** The part of `sharp` the logo read uses. */\ninterface SharpPixelPipeline {\n resize(width: number, height: number, options: { fit: 'inside' }): SharpPixelPipeline\n ensureAlpha(): SharpPixelPipeline\n raw(): SharpPixelPipeline\n toBuffer(options: { resolveWithObject: true }): Promise<{\n data: Buffer\n info: { channels: number }\n }>\n}\ntype SharpPixels = (input: Buffer, options?: { limitInputPixels?: number }) => SharpPixelPipeline\n\nconst LOGO_TYPES = /^image\\/(png|jpeg|webp|gif|avif|svg\\+xml)$/\n\n/**\n * A library logo's colors, read the way the media CDN would serve it\n * (`readAiMediaAssetBytes`). `null` when the asset may not be read or the bytes\n * cannot be decoded.\n *\n * On Vercel the read and the decode run in the plugins' image function, the\n * one function that ships `sharp`'s native library (`ai-image-reads.ts`,\n * AGL-4048), inside the brand reads' own bound (`signal`); anywhere else they\n * run here. Either way an answer that does not come is a logo whose colors\n * could not be read.\n */\nasync function readLogoColors(\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n signal?: AbortSignal,\n): Promise<string[] | null> {\n const images = await import('./ai-image-reads')\n const origin = images.aiImageReadsOrigin()\n if (!origin) return readAiThemeLogoColorsAt(firestore, location, hostId)\n // The location was held to the site's org already: an org asset's scope\n // IS that org, and a site's own asset needs none.\n const orgId = location.scope.isOrg ? location.scope.scopeId : ''\n const payload = images.aiImageReadsRequest('logo-colors', location, hostId, orgId)\n if (!payload) return null\n const answer = await images.requestAiImageReads<{ colors?: unknown }>(\n origin,\n payload,\n signal ?? AbortSignal.timeout(AI_THEME_BRAND_BUDGET_MS),\n )\n return logoColorsAnswer(answer?.colors)\n}\n\n/**\n * A library logo's colors read and decoded in THIS process, from a location\n * already held to the media library's rule: what `readLogoColors` does off\n * Vercel, and what the images function does for it on Vercel.\n */\nexport async function readAiThemeLogoColorsAt(\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n): Promise<string[] | null> {\n const asset = await readAiMediaAssetBytes(firestore, location, hostId, {\n maxBytes: AI_THEME_LOGO_MAX_BYTES,\n types: LOGO_TYPES,\n })\n if (!asset) return null\n const sharp = (await loadAiMediaSharp()) as SharpPixels\n const { data, info } = await sharp(asset.buffer, { limitInputPixels: 64_000_000 })\n .resize(48, 48, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n return dominantColorsFromPixels(data, info.channels)\n}\n\n/** What the images function answered, as hex colors: only colors leave it. */\nfunction logoColorsAnswer(colors: unknown): string[] | null {\n if (!Array.isArray(colors)) return null\n return colors\n .map((color) => (typeof color === 'string' ? normalizeThemeHex(color) : null))\n .filter((hex): hex is string => Boolean(hex))\n .slice(0, AI_THEME_BRAND_COLORS_PER_SOURCE)\n}\n\n/* ------------------------------------------------------------------------ *\n * A page the brief links to\n * ------------------------------------------------------------------------ */\n\nconst URL_IN_BRIEF = /\\bhttps?:\\/\\/[^\\s<>\"'`)\\]]+/i\n\n/** The first https link in a brief, or `null` — an http link is not followed. */\nexport function referenceUrlOf(brief: string): URL | null {\n const match = URL_IN_BRIEF.exec(brief)\n if (!match) return null\n try {\n const url = new URL(match[0].replace(/[.,;:!?]+$/, ''))\n return url.protocol === 'https:' ? url : null\n } catch {\n return null\n }\n}\n\n/** How much a page's declared `theme-color` counts beside one use of a color. */\nexport const AI_THEME_THEME_COLOR_WEIGHT = 25\n\nconst THEME_COLOR_META = /<meta\\b[^>]*\\bname\\s*=\\s*[\"']?theme-color\\b[^>]*>/gi\nconst CONTENT_ATTRIBUTE = /\\bcontent\\s*=\\s*[\"']?\\s*([^\"'\\s>]+)/i\n// A `#` after `&` is a character reference (`'`), not a color.\nconst HEX_LITERAL = /(?<!&)#(?:[0-9a-f]{6}|[0-9a-f]{3})(?![0-9a-z])/gi\nconst RGB_LITERAL = /rgba?\\(\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})\\s*[,\\s]\\s*(\\d{1,3})/gi\n\n/** Every color a page's markup or a stylesheet uses, weighted by how often. */\nexport function colorsFromMarkup(text: string): Map<string, number> {\n const weights = new Map<string, number>()\n const add = (hex: string | null, weight: number) => {\n if (hex) weights.set(hex, (weights.get(hex) ?? 0) + weight)\n }\n for (const tag of text.match(THEME_COLOR_META) ?? []) {\n add(normalizeThemeHex(CONTENT_ATTRIBUTE.exec(tag)?.[1] ?? ''), AI_THEME_THEME_COLOR_WEIGHT)\n }\n for (const match of text.matchAll(HEX_LITERAL)) add(normalizeThemeHex(match[0]), 1)\n for (const match of text.matchAll(RGB_LITERAL)) {\n add(toHex([Number(match[1]), Number(match[2]), Number(match[3])]), 1)\n }\n return weights\n}\n\nconst STYLESHEET_LINK = /<link\\b[^>]*\\brel\\s*=\\s*[\"']?stylesheet\\b[^>]*>/gi\nconst HREF_ATTRIBUTE = /\\bhref\\s*=\\s*[\"']?\\s*([^\"'\\s>]+)/i\n\n/** The https stylesheets a page links, resolved against it, at most a few. */\nexport function stylesheetUrlsOf(html: string, pageUrl: string): string[] {\n const urls: string[] = []\n for (const tag of html.match(STYLESHEET_LINK) ?? []) {\n const href = HREF_ATTRIBUTE.exec(tag)?.[1]\n if (!href) continue\n try {\n const url = new URL(href, pageUrl)\n if (url.protocol === 'https:' && !urls.includes(url.toString())) urls.push(url.toString())\n } catch {\n continue\n }\n if (urls.length === AI_THEME_REFERENCE_STYLESHEETS) break\n }\n return urls\n}\n\n/** The most used distinct colors, the colorful before the greys. */\nexport function rankBrandColors(\n weights: ReadonlyMap<string, number>,\n count = AI_THEME_BRAND_COLORS_PER_SOURCE,\n): string[] {\n const ranked = [...weights.entries()]\n .sort((a, b) => b[1] - a[1])\n .map(([hex]) => channelsOf(hex))\n return distinctColors(ranked, count)\n}\n\n/** One abort for the caller's signal and a clock, whichever ends first. */\nfunction deadline(signal: AbortSignal | undefined, ms: number) {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), ms)\n const onAbort = () => controller.abort()\n if (signal?.aborted) controller.abort()\n signal?.addEventListener('abort', onAbort)\n return {\n signal: controller.signal,\n done: () => {\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n },\n }\n}\n\n/**\n * The body as text, to `maxBytes`. Past the cap it is cut there, or with\n * `refuseOver` it is not read at all: `null`, the read canceled.\n */\nasync function readCapped(\n body: ReadableStream<Uint8Array>,\n maxBytes: number,\n refuseOver = false,\n binary = false,\n): Promise<{ text: string; bytes: number; truncated: boolean; data?: Uint8Array } | null> {\n const reader = body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n let truncated = false\n for (;;) {\n const { done, value } = await reader.read()\n if (done || !value) break\n const room = maxBytes - total\n if (value.byteLength > room) {\n await reader.cancel().catch(() => undefined)\n if (refuseOver) return null\n chunks.push(value.subarray(0, room))\n total += room\n truncated = true\n break\n }\n chunks.push(value)\n total += value.byteLength\n }\n const joined = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n joined.set(chunk, offset)\n offset += chunk.byteLength\n }\n // A picture's bytes are kept as they came (AGL-3976), never decoded as text.\n if (binary) return { text: '', bytes: total, truncated, data: joined }\n return { text: new TextDecoder().decode(joined), bytes: total, truncated }\n}\n\n/** Why {@link fetchPublicTextDetailed} read nothing. */\nexport type PublicFetchRefusal =\n /** Not https, a port, credentials, or not a URL at all. */\n | 'refused-url'\n /** The name resolves to a private address, or not at all. */\n | 'private-address'\n /** A redirect the caller's `redirectAllowed` refused. */\n | 'redirect-refused'\n | 'too-many-redirects'\n /** An answer that was not 2xx; `status` says which. */\n | 'http-status'\n /** A 2xx whose content type the caller does not accept. */\n | 'content-type'\n /** Past `maxBytes` with `refuseOver` set. */\n | 'too-large'\n /** The socket, the clock or the caller's signal ended it. */\n | 'network'\n\n/**\n * One shape for both outcomes, read by `ok`: a union does not narrow across\n * this repo's library boundaries (`strictNullChecks` is off), so a caller\n * could not reach either arm's fields.\n */\nexport interface PublicFetchResult {\n ok: boolean\n /** The address read, after redirects; or the one that was refused. */\n url: string\n /** Empty unless `ok`. */\n text: string\n bytes: number\n /** Cut at `maxBytes` (never with `refuseOver`). */\n truncated: boolean\n contentType: string\n /** Null when `ok`. */\n refusal: PublicFetchRefusal | null\n status?: number\n /** Where a refused redirect pointed. */\n location?: string\n /** The body's bytes, with `binary`; absent otherwise. */\n data?: Uint8Array\n /**\n * Whose edge refused an error answer, when its headers say (AGL-4083): the\n * bot protection a site owner opens for AglynBot is that edge's setting.\n */\n edge?: PublicFetchEdge\n}\n\n/** An edge network that names itself on the answers it refuses with. */\nexport type PublicFetchEdge = 'vercel' | 'cloudflare'\n\n/** The edge an answer came through, from the headers each one sets; `null` for neither. */\nexport function publicFetchEdgeOf(headers: Pick<Headers, 'get'>): PublicFetchEdge | null {\n const server = (headers.get('server') ?? '').toLowerCase()\n if (headers.get('x-vercel-mitigated') || headers.get('x-vercel-id') || server === 'vercel') return 'vercel'\n if (headers.get('cf-mitigated') || headers.get('cf-ray') || server === 'cloudflare') return 'cloudflare'\n return null\n}\n\nfunction refusedFetch(\n refusal: PublicFetchRefusal,\n url: string,\n extra: { status?: number; location?: string; edge?: PublicFetchEdge } = {},\n): PublicFetchResult {\n return { ok: false, url, text: '', bytes: 0, truncated: false, contentType: '', refusal, ...extra }\n}\n\nexport interface PublicFetchOptions {\n maxBytes: number\n accept: RegExp\n signal?: AbortSignal\n timeoutMs?: number\n /** Sent as `User-Agent`; the runtime's own when absent. */\n userAgent?: string\n /** Sent as `Accept`. */\n acceptHeader?: string\n /**\n * More request headers for one hop, asked again for EVERY hop's own address\n * (AGL-4083, AGL-4085), so a header meant for one host never rides a\n * redirect to another. Sent under `Accept` and `User-Agent`, and neither\n * can be changed through here. The site import's edge header goes this\n * way, and only to the hosts `siteImportEdgeHeadersFor` decided are ours.\n */\n headersFor?: (url: URL) => Readonly<Record<string, string>> | undefined\n /** Asked of every redirect hop before it is followed; the SSRF checks run as well. */\n redirectAllowed?: (from: URL, to: URL) => boolean\n /** Refuse a body over `maxBytes` rather than cut it. */\n refuseOver?: boolean\n /** Keep the body's bytes as `data` rather than decode them as text (a picture, AGL-3976). */\n binary?: boolean\n}\n\n/** The caller's extra headers, without any that would say what the request is: those are the fetch's own. */\nfunction publicFetchExtraHeaders(headers: Readonly<Record<string, string>> | undefined): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [name, value] of Object.entries(headers ?? {})) {\n if (/^(?:accept|user-agent|host|cookie|authorization|content-length)$/i.test(name)) continue\n if (typeof value === 'string') out[name] = value\n }\n return out\n}\n\n/**\n * A public https resource as text, through the plugin fetch proxy's SSRF\n * guard: the host must resolve only to public addresses and the socket is\n * pinned to the address checked; no credentials in the URL and no port but\n * the default; every redirect re-enters the same checks and the caller's own\n * `redirectAllowed`; the body is read to a byte cap under a clock, and only a\n * content type the caller accepts is read at all. Says why when it reads\n * nothing.\n */\nexport async function fetchPublicTextDetailed(\n url: string,\n options: PublicFetchOptions,\n): Promise<PublicFetchResult> {\n const { createPinnedDispatcher, resolvePublicIp } = await import('./ai-theme-brand-server')\n let current = url\n for (let hop = 0; hop <= MAX_REDIRECT_HOPS; hop += 1) {\n let parsed: URL\n try {\n parsed = new URL(current)\n } catch {\n return refusedFetch('refused-url', current)\n }\n if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {\n return refusedFetch('refused-url', current)\n }\n if (parsed.port && parsed.port !== '443') {\n return refusedFetch('refused-url', current)\n }\n const pinned = await resolvePublicIp(parsed.hostname)\n if (!pinned) return refusedFetch('private-address', parsed.toString())\n const dispatcher = createPinnedDispatcher(pinned)\n const clock = deadline(options.signal, options.timeoutMs ?? FETCH_TIMEOUT_MS)\n try {\n // `dispatcher` is undici's, which the global fetch honors; a variable\n // rather than a literal keeps the DOM `RequestInit` check off it.\n const init = {\n method: 'GET',\n redirect: 'manual' as const,\n signal: clock.signal,\n dispatcher,\n headers: {\n ...publicFetchExtraHeaders(options.headersFor?.(parsed)),\n Accept: options.acceptHeader ?? 'text/html, text/css;q=0.9',\n ...(options.userAgent ? { 'User-Agent': options.userAgent } : {}),\n },\n }\n const response = await fetch(parsed.toString(), init as RequestInit)\n if (REDIRECT_STATUSES.has(response.status)) {\n await response.body?.cancel().catch(() => undefined)\n const location = response.headers.get('location')\n if (!location) {\n return refusedFetch('http-status', parsed.toString(), { status: response.status })\n }\n let next: URL\n try {\n next = new URL(location, parsed)\n } catch {\n return refusedFetch('redirect-refused', parsed.toString(), { location })\n }\n if (options.redirectAllowed && !options.redirectAllowed(parsed, next)) {\n return refusedFetch('redirect-refused', parsed.toString(), { location: next.toString() })\n }\n current = next.toString()\n continue\n }\n if (!response.ok || !response.body) {\n await response.body?.cancel().catch(() => undefined)\n const edge = publicFetchEdgeOf(response.headers)\n return refusedFetch('http-status', parsed.toString(), { status: response.status, ...(edge ? { edge } : {}) })\n }\n const contentType = response.headers.get('content-type') ?? ''\n if (!options.accept.test(contentType)) {\n await response.body.cancel().catch(() => undefined)\n return refusedFetch('content-type', parsed.toString(), { status: response.status })\n }\n const declared = Number(response.headers.get('content-length') ?? '')\n if (options.refuseOver && Number.isFinite(declared) && declared > options.maxBytes) {\n await response.body.cancel().catch(() => undefined)\n return refusedFetch('too-large', parsed.toString(), { status: response.status })\n }\n const body = await readCapped(response.body, options.maxBytes, options.refuseOver, options.binary === true)\n if (!body) {\n return refusedFetch('too-large', parsed.toString(), { status: response.status })\n }\n return { ok: true, url: parsed.toString(), contentType, refusal: null, ...body }\n } catch {\n return refusedFetch('network', parsed.toString())\n } finally {\n clock.done()\n await dispatcher.close().catch(() => undefined)\n }\n }\n return refusedFetch('too-many-redirects', current)\n}\n\n/** {@link fetchPublicTextDetailed}, answering `null` for anything it refused. */\nexport async function fetchPublicText(\n url: string,\n options: { maxBytes: number; accept: RegExp; signal?: AbortSignal; timeoutMs?: number; headersFor?: PublicFetchOptions['headersFor'] },\n): Promise<{ text: string; url: string } | null> {\n const result = await fetchPublicTextDetailed(url, options)\n return result.ok ? { text: result.text, url: result.url } : null\n}\n\n/**\n * A public page as it was read: its markup and the text of its first\n * stylesheets ({@link AI_THEME_REFERENCE_STYLESHEETS}), each under its byte\n * cap. What a theme job takes its colors from and a site start its\n * inspiration signals (AGL-3975); never a crawl, one page and its sheets.\n */\nexport interface AiReferencePage {\n /** The address the markup was finally read at, after redirects. */\n url: string\n html: string\n stylesheets: string[]\n}\n\n/**\n * A page and its first stylesheets through {@link fetchPublicText}; `null` when the page cannot be read.\n * `headersFor` is asked for every request and every redirect hop, the page's\n * and each stylesheet's, by its own address (AGL-4083, AGL-4085): the site\n * import's edge header, built by `siteImportEdgeHeadersFor`, answers only for\n * the site's own origins we serve, so a hop or a stylesheet anywhere else (a\n * CDN's, another site's) carries nothing.\n */\nexport async function readReferencePage(\n url: URL,\n signal: AbortSignal,\n headersFor?: PublicFetchOptions['headersFor'],\n): Promise<AiReferencePage | null> {\n const page = await fetchPublicText(url.toString(), {\n maxBytes: AI_THEME_REFERENCE_MAX_BYTES,\n accept: /text\\/html|application\\/xhtml\\+xml/i,\n signal,\n ...(headersFor ? { headersFor } : {}),\n })\n if (!page) return null\n const stylesheets: string[] = []\n for (const sheet of stylesheetUrlsOf(page.text, page.url)) {\n const css = await fetchPublicText(sheet, {\n maxBytes: AI_THEME_STYLESHEET_MAX_BYTES,\n accept: /text\\/css/i,\n signal,\n ...(headersFor ? { headersFor } : {}),\n })\n if (css) stylesheets.push(css.text)\n }\n return { url: page.url, html: page.text, stylesheets }\n}\n\n/** A read page's brand colors: every color its markup and stylesheets use, ranked. */\nexport function referencePageColors(page: Pick<AiReferencePage, 'html' | 'stylesheets'>): string[] {\n const weights = colorsFromMarkup(page.html)\n for (const css of page.stylesheets) {\n for (const [hex, weight] of colorsFromMarkup(css)) {\n weights.set(hex, (weights.get(hex) ?? 0) + weight)\n }\n }\n return rankBrandColors(weights)\n}\n\n/** A linked page's colors, from its markup and its first stylesheets. */\nasync function readReferenceColors(url: URL, signal: AbortSignal): Promise<string[] | null> {\n const page = await readReferencePage(url, signal)\n return page ? referencePageColors(page) : null\n}\n\n/* ------------------------------------------------------------------------ *\n * Gathering\n * ------------------------------------------------------------------------ */\n\n/** The two reads that leave the process, replaceable where a spec needs to. */\nexport interface AiThemeBrandSeams {\n logoColors?: (\n firestore: FirebaseFirestore.Firestore,\n location: AiThemeLogoLocation,\n hostId: string,\n /** The brand reads' bound, which a read made elsewhere waits no longer than. */\n signal?: AbortSignal,\n ) => Promise<string[] | null>\n referenceColors?: (url: URL, signal: AbortSignal) => Promise<string[] | null>\n}\n\nexport async function gatherAiThemeBrandInputs(\n input: {\n firestore: FirebaseFirestore.Firestore\n org: Partial<AglynOrgBilling> | null\n hostId: string\n /** The site's host document. */\n host: Record<string, unknown>\n brief: string\n signal?: AbortSignal\n },\n seams: AiThemeBrandSeams = {},\n): Promise<AiThemeBrandInputs> {\n const colors: AiThemeBrandColor[] = [...organizationBrandColors(input.org)]\n const notes: string[] = []\n const clock = deadline(input.signal, AI_THEME_BRAND_BUDGET_MS)\n const add = (found: readonly string[], source: AiThemeBrandSource) => {\n for (const hex of found.slice(0, AI_THEME_BRAND_COLORS_PER_SOURCE)) {\n if (!colors.some((color) => color.hex === hex)) colors.push({ hex, source })\n }\n }\n try {\n const location = logoMediaLocation(\n input.host['logoUrl'],\n input.hostId,\n String(input.host['orgId'] ?? ''),\n )\n if (location) {\n const found = await (seams.logoColors ?? readLogoColors)(\n input.firestore,\n location,\n input.hostId,\n clock.signal,\n ).catch(() => null)\n if (found) add(found, 'logo')\n else notes.push(\"The site logo's colors could not be read, so the proposal does not use them.\")\n }\n const reference = referenceUrlOf(input.brief)\n if (reference) {\n const found = clock.signal.aborted\n ? null\n : await (seams.referenceColors ?? readReferenceColors)(reference, clock.signal).catch(\n () => null,\n )\n if (found?.length) add(found, 'reference')\n else {\n notes.push(\n `The page at ${reference.hostname} could not be read for its colors, so the proposal does not use them.`,\n )\n }\n }\n } finally {\n clock.done()\n }\n return { colors, notes }\n}\n"],"names":["checkEntitlement","normalizeThemeHex","AI_THEME_BRAND_BUDGET_MS","aiMediaAssetLocation","loadAiMediaSharp","readAiMediaAssetBytes","AI_THEME_BRAND_SOURCE_WORDS","organization","logo","reference","AI_THEME_BRAND_COLORS_PER_SOURCE","AI_THEME_LOGO_MAX_BYTES","AI_THEME_REFERENCE_MAX_BYTES","AI_THEME_STYLESHEET_MAX_BYTES","AI_THEME_REFERENCE_STYLESHEETS","FETCH_TIMEOUT_MS","MAX_REDIRECT_HOPS","REDIRECT_STATUSES","Set","toHex","red","green","blue","map","channel","Math","round","min","max","toString","padStart","join","channelsOf","hex","parseInt","slice","distance","a","b","hypot","chroma","SAME_COLOR_DISTANCE","GREY_CHROMA","distinctColors","ranked","count","colorful","filter","color","chosen","length","some","other","push","organizationBrandColors","org","raw","brandingProfile","primaryColor","source","logoMediaLocation","dominantColorsFromPixels","data","channels","buckets","Map","index","pixel","key","bucket","get","sum","pixels","set","values","sort","edge","inside","LOGO_TYPES","readLogoColors","firestore","location","hostId","signal","images","origin","aiImageReadsOrigin","readAiThemeLogoColorsAt","orgId","scope","isOrg","scopeId","payload","aiImageReadsRequest","answer","requestAiImageReads","AbortSignal","timeout","logoColorsAnswer","colors","asset","maxBytes","types","sharp","info","buffer","limitInputPixels","resize","fit","ensureAlpha","toBuffer","resolveWithObject","Array","isArray","Boolean","URL_IN_BRIEF","referenceUrlOf","brief","match","exec","url","URL","replace","protocol","AI_THEME_THEME_COLOR_WEIGHT","THEME_COLOR_META","CONTENT_ATTRIBUTE","HEX_LITERAL","RGB_LITERAL","colorsFromMarkup","text","weights","add","weight","tag","matchAll","Number","STYLESHEET_LINK","HREF_ATTRIBUTE","stylesheetUrlsOf","html","pageUrl","urls","href","includes","rankBrandColors","entries","deadline","ms","controller","AbortController","timer","setTimeout","abort","onAbort","aborted","addEventListener","done","clearTimeout","removeEventListener","readCapped","body","refuseOver","binary","reader","getReader","chunks","total","truncated","value","read","room","byteLength","cancel","catch","undefined","subarray","joined","Uint8Array","offset","chunk","bytes","TextDecoder","decode","publicFetchEdgeOf","headers","server","toLowerCase","refusedFetch","refusal","extra","ok","contentType","publicFetchExtraHeaders","out","name","Object","test","fetchPublicTextDetailed","options","createPinnedDispatcher","resolvePublicIp","current","hop","parsed","username","password","port","pinned","hostname","dispatcher","clock","timeoutMs","response","init","method","redirect","headersFor","Accept","acceptHeader","userAgent","fetch","has","status","next","redirectAllowed","accept","declared","isFinite","close","fetchPublicText","result","readReferencePage","page","stylesheets","sheet","css","referencePageColors","readReferenceColors","gatherAiThemeBrandInputs","input","seams","notes","found","host","String","logoColors","referenceColors"],"mappings":"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"}
|