@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.272

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.
Files changed (94) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +4 -3
  3. package/src/lib/activity/ai-activity-actions.js +3 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-site-build-page.component.js +18 -0
  6. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  7. package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
  8. package/src/lib/components/ai-site-import-job.component.js +707 -0
  9. package/src/lib/components/ai-site-import-job.component.js.map +1 -0
  10. package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
  11. package/src/lib/components/ai-site-import-start.component.js +335 -0
  12. package/src/lib/components/ai-site-import-start.component.js.map +1 -0
  13. package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
  14. package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
  15. package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
  16. package/src/lib/components/ai-site-start-card.component.js +165 -91
  17. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  18. package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
  19. package/src/lib/jobs/ai-job-drafts.js +24 -0
  20. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  21. package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
  22. package/src/lib/jobs/ai-job-import-step.js +809 -0
  23. package/src/lib/jobs/ai-job-import-step.js.map +1 -0
  24. package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
  25. package/src/lib/jobs/ai-job-page-language.js +62 -4
  26. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  27. package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
  28. package/src/lib/jobs/ai-job-page-step.js +5 -2
  29. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  31. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  32. package/src/lib/jobs/ai-jobs.d.ts +5 -0
  33. package/src/lib/jobs/ai-jobs.js +15 -3
  34. package/src/lib/jobs/ai-jobs.js.map +1 -1
  35. package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
  36. package/src/lib/jobs/ai-layout-owner-photos.js +47 -0
  37. package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
  38. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
  39. package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
  40. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  41. package/src/lib/model/ai-credit-estimate.d.ts +20 -0
  42. package/src/lib/model/ai-credit-estimate.js +13 -0
  43. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  44. package/src/lib/model/ai-job-activity.d.ts +2 -2
  45. package/src/lib/model/ai-job-activity.js +6 -4
  46. package/src/lib/model/ai-job-activity.js.map +1 -1
  47. package/src/lib/model/ai-job-notice.js +18 -4
  48. package/src/lib/model/ai-job-notice.js.map +1 -1
  49. package/src/lib/model/ai-jobs.types.d.ts +29 -2
  50. package/src/lib/model/ai-jobs.types.js +2 -1
  51. package/src/lib/model/ai-jobs.types.js.map +1 -1
  52. package/src/lib/model/ai-site-import-view.d.ts +130 -0
  53. package/src/lib/model/ai-site-import-view.js +182 -0
  54. package/src/lib/model/ai-site-import-view.js.map +1 -0
  55. package/src/lib/model/ai-site-import.d.ts +302 -0
  56. package/src/lib/model/ai-site-import.js +526 -0
  57. package/src/lib/model/ai-site-import.js.map +1 -0
  58. package/src/lib/model/ai-site-start.d.ts +22 -9
  59. package/src/lib/model/ai-site-start.js +64 -23
  60. package/src/lib/model/ai-site-start.js.map +1 -1
  61. package/src/lib/server/ai-jobs-route.js +11 -0
  62. package/src/lib/server/ai-jobs-route.js.map +1 -1
  63. package/src/lib/server/ai-site-import-route.d.ts +89 -0
  64. package/src/lib/server/ai-site-import-route.js +356 -0
  65. package/src/lib/server/ai-site-import-route.js.map +1 -0
  66. package/src/lib/server.js +14 -0
  67. package/src/lib/server.js.map +1 -1
  68. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
  69. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
  70. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
  71. package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
  72. package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
  73. package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
  74. package/src/lib/site-import/site-import-crawl.d.ts +163 -0
  75. package/src/lib/site-import/site-import-crawl.js +467 -0
  76. package/src/lib/site-import/site-import-crawl.js.map +1 -0
  77. package/src/lib/site-import/site-import-flag.d.ts +23 -0
  78. package/src/lib/site-import/site-import-flag.js +23 -0
  79. package/src/lib/site-import/site-import-flag.js.map +1 -0
  80. package/src/lib/site-import/site-import-html.d.ts +94 -0
  81. package/src/lib/site-import/site-import-html.js +462 -0
  82. package/src/lib/site-import/site-import-html.js.map +1 -0
  83. package/src/lib/site-import/site-import-images.d.ts +54 -0
  84. package/src/lib/site-import/site-import-images.js +204 -0
  85. package/src/lib/site-import/site-import-images.js.map +1 -0
  86. package/src/lib/site-import/site-import-page.d.ts +48 -0
  87. package/src/lib/site-import/site-import-page.js +150 -0
  88. package/src/lib/site-import/site-import-page.js.map +1 -0
  89. package/src/lib/site-import/site-import-review.d.ts +40 -0
  90. package/src/lib/site-import/site-import-review.js +149 -0
  91. package/src/lib/site-import/site-import-review.js.map +1 -0
  92. package/src/lib/site-import/site-import-robots.d.ts +52 -0
  93. package/src/lib/site-import/site-import-robots.js +126 -0
  94. package/src/lib/site-import/site-import-robots.js.map +1 -0
@@ -49,9 +49,24 @@ export declare function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean;
49
49
  * search, in slot order.
50
50
  */
51
51
  export declare function aiAssignOwnerPhotos(slots: readonly AiLayoutPictureSlot[], photos: readonly PluginMediaAsset[], avoid?: ReadonlySet<string>): Array<AiLayoutPicturePhoto | null>;
52
+ /** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */
53
+ export declare const AI_LAYOUT_PAGE_PHOTOS_INPUT = "pagePhotos";
54
+ /** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */
55
+ export declare function aiLayoutPagePhotosOf(inputs: Readonly<Record<string, unknown>> | null | undefined): PluginMediaAsset[];
56
+ /**
57
+ * The photos a page brought in from the owner's own site placed first
58
+ * (AGL-3976): the page's own pictures, already in the site's library, fill
59
+ * the slots they fit, in the same order an owner's photos do and with no
60
+ * job-wide repeat rule, since a team photo the old site showed twice is the
61
+ * owner's to show twice. The slots left go to `next`.
62
+ */
63
+ export declare function aiLayoutPagePhotoSource(photos: readonly PluginMediaAsset[], next: AiLayoutPictureSource | null): AiLayoutPictureSource | null;
52
64
  /**
53
65
  * The source for one page: the owner's photos for the slots they fit, the
54
66
  * stock library for the rest. `null` only when neither can answer — no
55
67
  * media door and no stock library — which leaves every slot to the starters.
68
+ * A page a site import brings in (AGL-3976) places its own pictures first.
56
69
  */
57
- export declare function aiLayoutSitePhotoSource(input: AiLayoutStockPhotoSourceInput, deps?: AiLayoutOwnerPhotoDeps): AiLayoutPictureSource | null;
70
+ export declare function aiLayoutSitePhotoSource(input: AiLayoutStockPhotoSourceInput & {
71
+ pagePhotos?: readonly PluginMediaAsset[];
72
+ }, deps?: AiLayoutOwnerPhotoDeps): AiLayoutPictureSource | null;
@@ -67,11 +67,58 @@ function area(photo) {
67
67
  }
68
68
  return answers;
69
69
  }
70
+ /** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */ export const AI_LAYOUT_PAGE_PHOTOS_INPUT = 'pagePhotos';
71
+ /** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */ export function aiLayoutPagePhotosOf(inputs) {
72
+ const raw = inputs == null ? void 0 : inputs[AI_LAYOUT_PAGE_PHOTOS_INPUT];
73
+ if (!Array.isArray(raw)) return [];
74
+ const photos = [];
75
+ for (const entry of raw){
76
+ if (!entry || typeof entry !== 'object') continue;
77
+ const { mediaId, src, width, height, alt } = entry;
78
+ if (typeof mediaId !== 'string' || typeof src !== 'string' || !src.startsWith('media:')) continue;
79
+ if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) continue;
80
+ photos.push(_extends({
81
+ mediaId,
82
+ src,
83
+ width,
84
+ height
85
+ }, typeof alt === 'string' && alt.trim() ? {
86
+ alt: alt.trim()
87
+ } : {}));
88
+ }
89
+ return photos;
90
+ }
91
+ /**
92
+ * The photos a page brought in from the owner's own site placed first
93
+ * (AGL-3976): the page's own pictures, already in the site's library, fill
94
+ * the slots they fit, in the same order an owner's photos do and with no
95
+ * job-wide repeat rule, since a team photo the old site showed twice is the
96
+ * owner's to show twice. The slots left go to `next`.
97
+ */ export function aiLayoutPagePhotoSource(photos, next) {
98
+ if (!photos.length) return next;
99
+ return async (slots)=>{
100
+ const answers = aiAssignOwnerPhotos(slots, photos);
101
+ if (!next) return answers;
102
+ const open = slots.filter((_slot, index)=>!answers[index]);
103
+ if (!open.length) return answers;
104
+ const found = await next(open);
105
+ let index = 0;
106
+ return answers.map((answer)=>{
107
+ var _ref;
108
+ return (_ref = answer != null ? answer : found[index++]) != null ? _ref : null;
109
+ });
110
+ };
111
+ }
70
112
  /**
71
113
  * The source for one page: the owner's photos for the slots they fit, the
72
114
  * stock library for the rest. `null` only when neither can answer — no
73
115
  * media door and no stock library — which leaves every slot to the starters.
116
+ * A page a site import brings in (AGL-3976) places its own pictures first.
74
117
  */ export function aiLayoutSitePhotoSource(input, deps = {}) {
118
+ var _input_pagePhotos;
119
+ return aiLayoutPagePhotoSource((_input_pagePhotos = input.pagePhotos) != null ? _input_pagePhotos : [], aiLayoutOwnerAndStockSource(input, deps));
120
+ }
121
+ function aiLayoutOwnerAndStockSource(input, deps) {
75
122
  var _deps_stock, _deps_ingest;
76
123
  var _ingest_listOwnerPhotos;
77
124
  const stock = ((_deps_stock = deps.stock) != null ? _deps_stock : aiLayoutStockPhotoSource)(input, deps);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-owner-photos.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 type { PluginMediaAsset } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { pluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport type {\n AiLayoutPicturePhoto,\n AiLayoutPictureSlot,\n AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\nimport {\n aiLayoutStockPhotoSource,\n type AiLayoutStockPhotoDeps,\n type AiLayoutStockPhotoSourceInput,\n} from './ai-layout-stock-photos'\n\n/**\n * THE BUSINESS'S OWN PHOTOS COME FIRST (AGL-3957).\n *\n * A site built for an owner at a booth or on a doorstep has, by the time its\n * pages are drawn, the photos they sent from their phone through a site\n * photo link: their van, their shop, their work. A photo of the owner's own\n * work beats any stock photo, so a page's picture slots are filled from\n * those first, and only the slots left over are searched for in the stock\n * library, the way they were before.\n *\n * Which photo goes where is decided by shape and size, not by subject: an\n * owner's photo says nothing a search could read. The hero takes the widest\n * photo among the largest; the about picture takes the next largest; the\n * gallery takes the rest, newest first, each photo once on the page and once\n * across the job (`avoid`). A product's photo, a person's portrait and a\n * record's cover are of one named thing, which a photo of the business in\n * general is not, so those slots go to the stock search untouched.\n */\n\nexport interface AiLayoutOwnerPhotoDeps extends AiLayoutStockPhotoDeps {\n /** The stock source the leftover slots go to; the real one otherwise. Specs hand in a fake. */\n stock?: typeof aiLayoutStockPhotoSource\n /** The most owner photos read for one page. */\n limit?: number\n}\n\nconst OWNER_PHOTOS_LIMIT = 60\n\n/** A slot an owner's photo may fill: one of the business in general, not of one named thing. */\nexport function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean {\n return !slot.product && !slot.people && !slot.record\n}\n\n/** Fill order: the hero first, so the biggest photo is the first one seen; then about; then the gallery in document order. */\nconst ROLE_RANK = { hero: 0, about: 1, gallery: 2 } as const\n\nfunction area(photo: PluginMediaAsset): number {\n return (photo.width ?? 0) * (photo.height ?? 0)\n}\n\n/**\n * Assigns the owner's photos to the slots, in the order above. Pure, so a\n * spec can hold the choice itself: `null` for a slot left to the stock\n * search, in slot order.\n */\nexport function aiAssignOwnerPhotos(\n slots: readonly AiLayoutPictureSlot[],\n photos: readonly PluginMediaAsset[],\n avoid: ReadonlySet<string> = new Set(),\n): Array<AiLayoutPicturePhoto | null> {\n const pool = photos\n .filter((photo) => photo.src && (photo.width ?? 0) > 0 && (photo.height ?? 0) > 0 && !avoid.has(photo.src))\n .sort((a, b) => area(b) - area(a))\n const answers: Array<AiLayoutPicturePhoto | null> = slots.map(() => null)\n const used = new Set<string>()\n const order = slots\n .map((slot, index) => ({ slot, index }))\n .filter(({ slot }) => aiOwnerPhotoFits(slot))\n .sort((a, b) => ROLE_RANK[a.slot.role] - ROLE_RANK[b.slot.role] || a.index - b.index)\n for (const { slot, index } of order) {\n const free = pool.filter((photo) => !used.has(photo.src))\n if (!free.length) break\n // The hero wants the widest of the three largest; a portrait phone photo\n // in a wide frame is the one mismatch a visitor sees first.\n const pick =\n slot.role === 'hero'\n ? [...free.slice(0, 3)].sort(\n (a, b) => (b.width ?? 0) / (b.height ?? 1) - (a.width ?? 0) / (a.height ?? 1),\n )[0]\n : free[0]\n if (!pick) break\n used.add(pick.src)\n answers[index] = {\n src: pick.src,\n width: pick.width as number,\n height: pick.height as number,\n ...(pick.alt ? { alt: pick.alt } : {}),\n }\n }\n return answers\n}\n\n/**\n * The source for one page: the owner's photos for the slots they fit, the\n * stock library for the rest. `null` only when neither can answer — no\n * media door and no stock library — which leaves every slot to the starters.\n */\nexport function aiLayoutSitePhotoSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutOwnerPhotoDeps = {},\n): AiLayoutPictureSource | null {\n const stock = (deps.stock ?? aiLayoutStockPhotoSource)(input, deps)\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n const list = ingest?.listOwnerPhotos?.bind(ingest)\n if (!list || !input.hostId) return stock\n return async (slots) => {\n let photos: PluginMediaAsset[] = []\n try {\n photos = await list({ hostId: input.hostId, limit: deps.limit ?? OWNER_PHOTOS_LIMIT })\n } catch (error) {\n console.warn('ai layout pictures: the owner photos could not be read; stock photos fill the page', { error })\n }\n const answers = aiAssignOwnerPhotos(slots, photos, new Set(input.avoid ?? []))\n if (!stock) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n // The stock source sees only the slots the owner's photos left, so its\n // own no-repeat rules and search budget are spent on those alone.\n const found = await stock(open)\n let next = 0\n return answers.map((answer) => answer ?? found[next++] ?? null)\n }\n}\n"],"names":["pluginMediaIngest","aiLayoutStockPhotoSource","OWNER_PHOTOS_LIMIT","aiOwnerPhotoFits","slot","product","people","record","ROLE_RANK","hero","about","gallery","area","photo","width","height","aiAssignOwnerPhotos","slots","photos","avoid","Set","pool","filter","src","has","sort","a","b","answers","map","used","order","index","role","free","length","pick","slice","add","alt","aiLayoutSitePhotoSource","input","deps","ingest","stock","list","listOwnerPhotos","bind","hostId","limit","error","console","warn","open","_slot","found","next","answer"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-layout-owner-photos.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 type { PluginMediaAsset } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport { pluginMediaIngest } from '@aglyn/aglyn/plugin-manager/plugin-media-ingest'\nimport type {\n AiLayoutPicturePhoto,\n AiLayoutPictureSlot,\n AiLayoutPictureSource,\n} from '../layout-language/ai-layout-pictures'\nimport {\n aiLayoutStockPhotoSource,\n type AiLayoutStockPhotoDeps,\n type AiLayoutStockPhotoSourceInput,\n} from './ai-layout-stock-photos'\n\n/**\n * THE BUSINESS'S OWN PHOTOS COME FIRST (AGL-3957).\n *\n * A site built for an owner at a booth or on a doorstep has, by the time its\n * pages are drawn, the photos they sent from their phone through a site\n * photo link: their van, their shop, their work. A photo of the owner's own\n * work beats any stock photo, so a page's picture slots are filled from\n * those first, and only the slots left over are searched for in the stock\n * library, the way they were before.\n *\n * Which photo goes where is decided by shape and size, not by subject: an\n * owner's photo says nothing a search could read. The hero takes the widest\n * photo among the largest; the about picture takes the next largest; the\n * gallery takes the rest, newest first, each photo once on the page and once\n * across the job (`avoid`). A product's photo, a person's portrait and a\n * record's cover are of one named thing, which a photo of the business in\n * general is not, so those slots go to the stock search untouched.\n */\n\nexport interface AiLayoutOwnerPhotoDeps extends AiLayoutStockPhotoDeps {\n /** The stock source the leftover slots go to; the real one otherwise. Specs hand in a fake. */\n stock?: typeof aiLayoutStockPhotoSource\n /** The most owner photos read for one page. */\n limit?: number\n}\n\nconst OWNER_PHOTOS_LIMIT = 60\n\n/** A slot an owner's photo may fill: one of the business in general, not of one named thing. */\nexport function aiOwnerPhotoFits(slot: AiLayoutPictureSlot): boolean {\n return !slot.product && !slot.people && !slot.record\n}\n\n/** Fill order: the hero first, so the biggest photo is the first one seen; then about; then the gallery in document order. */\nconst ROLE_RANK = { hero: 0, about: 1, gallery: 2 } as const\n\nfunction area(photo: PluginMediaAsset): number {\n return (photo.width ?? 0) * (photo.height ?? 0)\n}\n\n/**\n * Assigns the owner's photos to the slots, in the order above. Pure, so a\n * spec can hold the choice itself: `null` for a slot left to the stock\n * search, in slot order.\n */\nexport function aiAssignOwnerPhotos(\n slots: readonly AiLayoutPictureSlot[],\n photos: readonly PluginMediaAsset[],\n avoid: ReadonlySet<string> = new Set(),\n): Array<AiLayoutPicturePhoto | null> {\n const pool = photos\n .filter((photo) => photo.src && (photo.width ?? 0) > 0 && (photo.height ?? 0) > 0 && !avoid.has(photo.src))\n .sort((a, b) => area(b) - area(a))\n const answers: Array<AiLayoutPicturePhoto | null> = slots.map(() => null)\n const used = new Set<string>()\n const order = slots\n .map((slot, index) => ({ slot, index }))\n .filter(({ slot }) => aiOwnerPhotoFits(slot))\n .sort((a, b) => ROLE_RANK[a.slot.role] - ROLE_RANK[b.slot.role] || a.index - b.index)\n for (const { slot, index } of order) {\n const free = pool.filter((photo) => !used.has(photo.src))\n if (!free.length) break\n // The hero wants the widest of the three largest; a portrait phone photo\n // in a wide frame is the one mismatch a visitor sees first.\n const pick =\n slot.role === 'hero'\n ? [...free.slice(0, 3)].sort(\n (a, b) => (b.width ?? 0) / (b.height ?? 1) - (a.width ?? 0) / (a.height ?? 1),\n )[0]\n : free[0]\n if (!pick) break\n used.add(pick.src)\n answers[index] = {\n src: pick.src,\n width: pick.width as number,\n height: pick.height as number,\n ...(pick.alt ? { alt: pick.alt } : {}),\n }\n }\n return answers\n}\n\n/** The unit input carrying the photos a page brought in from the owner's own site (AGL-3976). */\nexport const AI_LAYOUT_PAGE_PHOTOS_INPUT = 'pagePhotos'\n\n/** The page's own photos a job's inputs carry, read defensively: library assets with their sizes only. */\nexport function aiLayoutPagePhotosOf(inputs: Readonly<Record<string, unknown>> | null | undefined): PluginMediaAsset[] {\n const raw = inputs?.[AI_LAYOUT_PAGE_PHOTOS_INPUT]\n if (!Array.isArray(raw)) return []\n const photos: PluginMediaAsset[] = []\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const { mediaId, src, width, height, alt } = entry as Record<string, unknown>\n if (typeof mediaId !== 'string' || typeof src !== 'string' || !src.startsWith('media:')) continue\n if (typeof width !== 'number' || typeof height !== 'number' || width <= 0 || height <= 0) continue\n photos.push({ mediaId, src, width, height, ...(typeof alt === 'string' && alt.trim() ? { alt: alt.trim() } : {}) })\n }\n return photos\n}\n\n/**\n * The photos a page brought in from the owner's own site placed first\n * (AGL-3976): the page's own pictures, already in the site's library, fill\n * the slots they fit, in the same order an owner's photos do and with no\n * job-wide repeat rule, since a team photo the old site showed twice is the\n * owner's to show twice. The slots left go to `next`.\n */\nexport function aiLayoutPagePhotoSource(\n photos: readonly PluginMediaAsset[],\n next: AiLayoutPictureSource | null,\n): AiLayoutPictureSource | null {\n if (!photos.length) return next\n return async (slots) => {\n const answers = aiAssignOwnerPhotos(slots, photos)\n if (!next) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n const found = await next(open)\n let index = 0\n return answers.map((answer) => answer ?? found[index++] ?? null)\n }\n}\n\n/**\n * The source for one page: the owner's photos for the slots they fit, the\n * stock library for the rest. `null` only when neither can answer — no\n * media door and no stock library — which leaves every slot to the starters.\n * A page a site import brings in (AGL-3976) places its own pictures first.\n */\nexport function aiLayoutSitePhotoSource(\n input: AiLayoutStockPhotoSourceInput & { pagePhotos?: readonly PluginMediaAsset[] },\n deps: AiLayoutOwnerPhotoDeps = {},\n): AiLayoutPictureSource | null {\n return aiLayoutPagePhotoSource(input.pagePhotos ?? [], aiLayoutOwnerAndStockSource(input, deps))\n}\n\nfunction aiLayoutOwnerAndStockSource(\n input: AiLayoutStockPhotoSourceInput,\n deps: AiLayoutOwnerPhotoDeps,\n): AiLayoutPictureSource | null {\n const stock = (deps.stock ?? aiLayoutStockPhotoSource)(input, deps)\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n const list = ingest?.listOwnerPhotos?.bind(ingest)\n if (!list || !input.hostId) return stock\n return async (slots) => {\n let photos: PluginMediaAsset[] = []\n try {\n photos = await list({ hostId: input.hostId, limit: deps.limit ?? OWNER_PHOTOS_LIMIT })\n } catch (error) {\n console.warn('ai layout pictures: the owner photos could not be read; stock photos fill the page', { error })\n }\n const answers = aiAssignOwnerPhotos(slots, photos, new Set(input.avoid ?? []))\n if (!stock) return answers\n const open = slots.filter((_slot, index) => !answers[index])\n if (!open.length) return answers\n // The stock source sees only the slots the owner's photos left, so its\n // own no-repeat rules and search budget are spent on those alone.\n const found = await stock(open)\n let next = 0\n return answers.map((answer) => answer ?? found[next++] ?? null)\n }\n}\n"],"names":["pluginMediaIngest","aiLayoutStockPhotoSource","OWNER_PHOTOS_LIMIT","aiOwnerPhotoFits","slot","product","people","record","ROLE_RANK","hero","about","gallery","area","photo","width","height","aiAssignOwnerPhotos","slots","photos","avoid","Set","pool","filter","src","has","sort","a","b","answers","map","used","order","index","role","free","length","pick","slice","add","alt","AI_LAYOUT_PAGE_PHOTOS_INPUT","aiLayoutPagePhotosOf","inputs","raw","Array","isArray","entry","mediaId","startsWith","push","trim","aiLayoutPagePhotoSource","next","open","_slot","found","answer","aiLayoutSitePhotoSource","input","deps","pagePhotos","aiLayoutOwnerAndStockSource","ingest","stock","list","listOwnerPhotos","bind","hostId","limit","error","console","warn"],"mappings":"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"}
@@ -92,14 +92,72 @@ export declare function colorsFromMarkup(text: string): Map<string, number>;
92
92
  export declare function stylesheetUrlsOf(html: string, pageUrl: string): string[];
93
93
  /** The most used distinct colors, the colorful before the greys. */
94
94
  export declare function rankBrandColors(weights: ReadonlyMap<string, number>, count?: number): string[];
95
+ /** Why {@link fetchPublicTextDetailed} read nothing. */
96
+ export type PublicFetchRefusal =
97
+ /** Not https, a port, credentials, or not a URL at all. */
98
+ 'refused-url'
99
+ /** The name resolves to a private address, or not at all. */
100
+ | 'private-address'
101
+ /** A redirect the caller's `redirectAllowed` refused. */
102
+ | 'redirect-refused' | 'too-many-redirects'
103
+ /** An answer that was not 2xx; `status` says which. */
104
+ | 'http-status'
105
+ /** A 2xx whose content type the caller does not accept. */
106
+ | 'content-type'
107
+ /** Past `maxBytes` with `refuseOver` set. */
108
+ | 'too-large'
109
+ /** The socket, the clock or the caller's signal ended it. */
110
+ | 'network';
111
+ /**
112
+ * One shape for both outcomes, read by `ok`: a union does not narrow across
113
+ * this repo's library boundaries (`strictNullChecks` is off), so a caller
114
+ * could not reach either arm's fields.
115
+ */
116
+ export interface PublicFetchResult {
117
+ ok: boolean;
118
+ /** The address read, after redirects; or the one that was refused. */
119
+ url: string;
120
+ /** Empty unless `ok`. */
121
+ text: string;
122
+ bytes: number;
123
+ /** Cut at `maxBytes` (never with `refuseOver`). */
124
+ truncated: boolean;
125
+ contentType: string;
126
+ /** Null when `ok`. */
127
+ refusal: PublicFetchRefusal | null;
128
+ status?: number;
129
+ /** Where a refused redirect pointed. */
130
+ location?: string;
131
+ /** The body's bytes, with `binary`; absent otherwise. */
132
+ data?: Uint8Array;
133
+ }
134
+ export interface PublicFetchOptions {
135
+ maxBytes: number;
136
+ accept: RegExp;
137
+ signal?: AbortSignal;
138
+ timeoutMs?: number;
139
+ /** Sent as `User-Agent`; the runtime's own when absent. */
140
+ userAgent?: string;
141
+ /** Sent as `Accept`. */
142
+ acceptHeader?: string;
143
+ /** Asked of every redirect hop before it is followed; the SSRF checks run as well. */
144
+ redirectAllowed?: (from: URL, to: URL) => boolean;
145
+ /** Refuse a body over `maxBytes` rather than cut it. */
146
+ refuseOver?: boolean;
147
+ /** Keep the body's bytes as `data` rather than decode them as text (a picture, AGL-3976). */
148
+ binary?: boolean;
149
+ }
95
150
  /**
96
151
  * A public https resource as text, through the plugin fetch proxy's SSRF
97
152
  * guard: the host must resolve only to public addresses and the socket is
98
153
  * pinned to the address checked; no credentials in the URL and no port but
99
- * the default; every redirect re-enters the same checks; the body is read to
100
- * a byte cap under a clock, and only a content type the caller accepts is
101
- * read at all. `null` for anything that does not hold.
154
+ * the default; every redirect re-enters the same checks and the caller's own
155
+ * `redirectAllowed`; the body is read to a byte cap under a clock, and only a
156
+ * content type the caller accepts is read at all. Says why when it reads
157
+ * nothing.
102
158
  */
159
+ export declare function fetchPublicTextDetailed(url: string, options: PublicFetchOptions): Promise<PublicFetchResult>;
160
+ /** {@link fetchPublicTextDetailed}, answering `null` for anything it refused. */
103
161
  export declare function fetchPublicText(url: string, options: {
104
162
  maxBytes: number;
105
163
  accept: RegExp;
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -229,38 +230,68 @@ const HREF_ATTRIBUTE = /\bhref\s*=\s*["']?\s*([^"'\s>]+)/i;
229
230
  }
230
231
  };
231
232
  }
232
- async function readCapped(body, maxBytes) {
233
+ /**
234
+ * The body as text, to `maxBytes`. Past the cap it is cut there, or with
235
+ * `refuseOver` it is not read at all: `null`, the read canceled.
236
+ */ async function readCapped(body, maxBytes, refuseOver = false, binary = false) {
233
237
  const reader = body.getReader();
234
238
  const chunks = [];
235
239
  let total = 0;
240
+ let truncated = false;
236
241
  for(;;){
237
242
  const { done, value } = await reader.read();
238
243
  if (done || !value) break;
239
244
  const room = maxBytes - total;
240
- if (value.byteLength >= room) {
241
- chunks.push(value.subarray(0, room));
245
+ if (value.byteLength > room) {
242
246
  await reader.cancel().catch(()=>undefined);
247
+ if (refuseOver) return null;
248
+ chunks.push(value.subarray(0, room));
249
+ total += room;
250
+ truncated = true;
243
251
  break;
244
252
  }
245
253
  chunks.push(value);
246
254
  total += value.byteLength;
247
255
  }
248
- const joined = new Uint8Array(chunks.reduce((sum, chunk)=>sum + chunk.byteLength, 0));
256
+ const joined = new Uint8Array(total);
249
257
  let offset = 0;
250
258
  for (const chunk of chunks){
251
259
  joined.set(chunk, offset);
252
260
  offset += chunk.byteLength;
253
261
  }
254
- return new TextDecoder().decode(joined);
262
+ // A picture's bytes are kept as they came (AGL-3976), never decoded as text.
263
+ if (binary) return {
264
+ text: '',
265
+ bytes: total,
266
+ truncated,
267
+ data: joined
268
+ };
269
+ return {
270
+ text: new TextDecoder().decode(joined),
271
+ bytes: total,
272
+ truncated
273
+ };
274
+ }
275
+ function refusedFetch(refusal, url, extra = {}) {
276
+ return _extends({
277
+ ok: false,
278
+ url,
279
+ text: '',
280
+ bytes: 0,
281
+ truncated: false,
282
+ contentType: '',
283
+ refusal
284
+ }, extra);
255
285
  }
256
286
  /**
257
287
  * A public https resource as text, through the plugin fetch proxy's SSRF
258
288
  * guard: the host must resolve only to public addresses and the socket is
259
289
  * pinned to the address checked; no credentials in the URL and no port but
260
- * the default; every redirect re-enters the same checks; the body is read to
261
- * a byte cap under a clock, and only a content type the caller accepts is
262
- * read at all. `null` for anything that does not hold.
263
- */ export async function fetchPublicText(url, options) {
290
+ * the default; every redirect re-enters the same checks and the caller's own
291
+ * `redirectAllowed`; the body is read to a byte cap under a clock, and only a
292
+ * content type the caller accepts is read at all. Says why when it reads
293
+ * nothing.
294
+ */ export async function fetchPublicTextDetailed(url, options) {
264
295
  const { createPinnedDispatcher, resolvePublicIp } = await import("./ai-theme-brand-server.js");
265
296
  let current = url;
266
297
  for(let hop = 0; hop <= MAX_REDIRECT_HOPS; hop += 1){
@@ -269,16 +300,20 @@ async function readCapped(body, maxBytes) {
269
300
  try {
270
301
  parsed = new URL(current);
271
302
  } catch (unused) {
272
- return null;
303
+ return refusedFetch('refused-url', current);
304
+ }
305
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
306
+ return refusedFetch('refused-url', current);
307
+ }
308
+ if (parsed.port && parsed.port !== '443') {
309
+ return refusedFetch('refused-url', current);
273
310
  }
274
- if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null;
275
- if (parsed.port && parsed.port !== '443') return null;
276
311
  const pinned = await resolvePublicIp(parsed.hostname);
277
- if (!pinned) return null;
312
+ if (!pinned) return refusedFetch('private-address', parsed.toString());
278
313
  const dispatcher = createPinnedDispatcher(pinned);
279
314
  const clock = deadline(options.signal, (_options_timeoutMs = options.timeoutMs) != null ? _options_timeoutMs : FETCH_TIMEOUT_MS);
280
315
  try {
281
- var _response_headers_get;
316
+ var _options_acceptHeader, _response_headers_get, _response_headers_get1;
282
317
  // `dispatcher` is undici's, which the global fetch honors; a variable
283
318
  // rather than a literal keeps the DOM `RequestInit` check off it.
284
319
  const init = {
@@ -286,31 +321,86 @@ async function readCapped(body, maxBytes) {
286
321
  redirect: 'manual',
287
322
  signal: clock.signal,
288
323
  dispatcher,
289
- headers: {
290
- Accept: 'text/html, text/css;q=0.9'
291
- }
324
+ headers: _extends({
325
+ Accept: (_options_acceptHeader = options.acceptHeader) != null ? _options_acceptHeader : 'text/html, text/css;q=0.9'
326
+ }, options.userAgent ? {
327
+ 'User-Agent': options.userAgent
328
+ } : {})
292
329
  };
293
330
  const response = await fetch(parsed.toString(), init);
294
331
  if (REDIRECT_STATUSES.has(response.status)) {
332
+ var _response_body;
333
+ await ((_response_body = response.body) == null ? void 0 : _response_body.cancel().catch(()=>undefined));
295
334
  const location = response.headers.get('location');
296
- if (!location) return null;
297
- current = new URL(location, parsed).toString();
335
+ if (!location) {
336
+ return refusedFetch('http-status', parsed.toString(), {
337
+ status: response.status
338
+ });
339
+ }
340
+ let next;
341
+ try {
342
+ next = new URL(location, parsed);
343
+ } catch (unused) {
344
+ return refusedFetch('redirect-refused', parsed.toString(), {
345
+ location
346
+ });
347
+ }
348
+ if (options.redirectAllowed && !options.redirectAllowed(parsed, next)) {
349
+ return refusedFetch('redirect-refused', parsed.toString(), {
350
+ location: next.toString()
351
+ });
352
+ }
353
+ current = next.toString();
298
354
  continue;
299
355
  }
300
- if (!response.ok || !response.body) return null;
301
- if (!options.accept.test((_response_headers_get = response.headers.get('content-type')) != null ? _response_headers_get : '')) return null;
302
- return {
303
- text: await readCapped(response.body, options.maxBytes),
304
- url: parsed.toString()
305
- };
356
+ if (!response.ok || !response.body) {
357
+ var _response_body1;
358
+ await ((_response_body1 = response.body) == null ? void 0 : _response_body1.cancel().catch(()=>undefined));
359
+ return refusedFetch('http-status', parsed.toString(), {
360
+ status: response.status
361
+ });
362
+ }
363
+ const contentType = (_response_headers_get = response.headers.get('content-type')) != null ? _response_headers_get : '';
364
+ if (!options.accept.test(contentType)) {
365
+ await response.body.cancel().catch(()=>undefined);
366
+ return refusedFetch('content-type', parsed.toString(), {
367
+ status: response.status
368
+ });
369
+ }
370
+ const declared = Number((_response_headers_get1 = response.headers.get('content-length')) != null ? _response_headers_get1 : '');
371
+ if (options.refuseOver && Number.isFinite(declared) && declared > options.maxBytes) {
372
+ await response.body.cancel().catch(()=>undefined);
373
+ return refusedFetch('too-large', parsed.toString(), {
374
+ status: response.status
375
+ });
376
+ }
377
+ const body = await readCapped(response.body, options.maxBytes, options.refuseOver, options.binary === true);
378
+ if (!body) {
379
+ return refusedFetch('too-large', parsed.toString(), {
380
+ status: response.status
381
+ });
382
+ }
383
+ return _extends({
384
+ ok: true,
385
+ url: parsed.toString(),
386
+ contentType,
387
+ refusal: null
388
+ }, body);
306
389
  } catch (unused) {
307
- return null;
390
+ return refusedFetch('network', parsed.toString());
308
391
  } finally{
309
392
  clock.done();
310
393
  await dispatcher.close().catch(()=>undefined);
311
394
  }
312
395
  }
313
- return null;
396
+ return refusedFetch('too-many-redirects', current);
397
+ }
398
+ /** {@link fetchPublicTextDetailed}, answering `null` for anything it refused. */ export async function fetchPublicText(url, options) {
399
+ const result = await fetchPublicTextDetailed(url, options);
400
+ return result.ok ? {
401
+ text: result.text,
402
+ url: result.url
403
+ } : null;
314
404
  }
315
405
  /** A page and its first stylesheets through {@link fetchPublicText}; `null` when the page cannot be read. */ export async function readReferencePage(url, signal) {
316
406
  const page = await fetchPublicText(url.toString(), {
@@ -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 */\nasync function readLogoColors(\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/* ------------------------------------------------------------------------ *\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 (`&#039;`), 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\nasync function readCapped(body: ReadableStream<Uint8Array>, maxBytes: number): Promise<string> {\n const reader = body.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\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 chunks.push(value.subarray(0, room))\n await reader.cancel().catch(() => undefined)\n break\n }\n chunks.push(value)\n total += value.byteLength\n }\n const joined = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0))\n let offset = 0\n for (const chunk of chunks) {\n joined.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(joined)\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; the body is read to\n * a byte cap under a clock, and only a content type the caller accepts is\n * read at all. `null` for anything that does not hold.\n */\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 { 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 null\n }\n if (parsed.protocol !== 'https:' || parsed.username || parsed.password) return null\n if (parsed.port && parsed.port !== '443') return null\n const pinned = await resolvePublicIp(parsed.hostname)\n if (!pinned) return null\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: { Accept: 'text/html, text/css;q=0.9' },\n }\n const response = await fetch(parsed.toString(), init as RequestInit)\n if (REDIRECT_STATUSES.has(response.status)) {\n const location = response.headers.get('location')\n if (!location) return null\n current = new URL(location, parsed).toString()\n continue\n }\n if (!response.ok || !response.body) return null\n if (!options.accept.test(response.headers.get('content-type') ?? '')) return null\n return { text: await readCapped(response.body, options.maxBytes), url: parsed.toString() }\n } catch {\n return null\n } finally {\n clock.done()\n await dispatcher.close().catch(() => undefined)\n }\n }\n return 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 ) => 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 ).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","asset","maxBytes","types","sharp","info","buffer","limitInputPixels","resize","fit","ensureAlpha","toBuffer","resolveWithObject","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","signal","ms","controller","AbortController","timer","setTimeout","abort","onAbort","aborted","addEventListener","done","clearTimeout","removeEventListener","readCapped","body","reader","getReader","chunks","total","value","read","room","byteLength","subarray","cancel","catch","undefined","joined","Uint8Array","reduce","chunk","offset","TextDecoder","decode","fetchPublicText","options","createPinnedDispatcher","resolvePublicIp","current","hop","parsed","username","password","port","pinned","hostname","dispatcher","clock","timeoutMs","response","init","method","redirect","headers","Accept","fetch","has","status","ok","accept","test","close","readReferencePage","page","stylesheets","sheet","css","referencePageColors","readReferenceColors","gatherAiThemeBrandInputs","input","seams","colors","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 */\nasync function readLogoColors(\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/* ------------------------------------------------------------------------ *\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 (`&#039;`), 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 ) => 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 ).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","asset","maxBytes","types","sharp","info","buffer","limitInputPixels","resize","fit","ensureAlpha","toBuffer","resolveWithObject","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","signal","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","colors","notes","found","host","String","logoColors","referenceColors"],"mappings":"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"}