@aglyn/plugins-ai 1.0.0-beta.284 → 1.0.0-beta.287
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/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
|
@@ -70,9 +70,23 @@ export declare function encodeAiProductImage(bytes: Buffer): Promise<Buffer>;
|
|
|
70
70
|
* The product's first photo as a picture part, or what kept it from being
|
|
71
71
|
* one. Never throws: a photo that cannot be read leaves the copy to be written
|
|
72
72
|
* from the product's words, and says so.
|
|
73
|
+
*
|
|
74
|
+
* On Vercel the read and the encode run in the plugins' image function, the
|
|
75
|
+
* one function that ships `sharp`'s native library (`ai-image-reads.ts`,
|
|
76
|
+
* AGL-4048), bounded by `AI_PRODUCT_IMAGE_READ_MS`, the time a pass counts
|
|
77
|
+
* for it; anywhere else, and wherever a caller hands its own reads (a spec,
|
|
78
|
+
* the eval harness), they run here. Either way an answer that does not come
|
|
79
|
+
* is an unreadable photo.
|
|
73
80
|
*/
|
|
74
81
|
export declare function readAiProductImage(firestore: FirebaseFirestore.Firestore, input: {
|
|
75
82
|
value: string | null | undefined;
|
|
76
83
|
hostId: string;
|
|
77
84
|
orgId: string;
|
|
78
85
|
}, seams?: AiProductImageSeams): Promise<AiProductImage>;
|
|
86
|
+
/**
|
|
87
|
+
* The photo read and encoded in THIS process, from a location already held
|
|
88
|
+
* to the media library's rule: what `readAiProductImage` does off Vercel,
|
|
89
|
+
* and what the images function does for it on Vercel. Throws when the read
|
|
90
|
+
* or the encode does.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readAiProductImageAt(firestore: FirebaseFirestore.Firestore, location: AiMediaAssetLocation, hostId: string, seams?: AiProductImageSeams): Promise<AiProductImage>;
|
|
@@ -69,6 +69,13 @@
|
|
|
69
69
|
* The product's first photo as a picture part, or what kept it from being
|
|
70
70
|
* one. Never throws: a photo that cannot be read leaves the copy to be written
|
|
71
71
|
* from the product's words, and says so.
|
|
72
|
+
*
|
|
73
|
+
* On Vercel the read and the encode run in the plugins' image function, the
|
|
74
|
+
* one function that ships `sharp`'s native library (`ai-image-reads.ts`,
|
|
75
|
+
* AGL-4048), bounded by `AI_PRODUCT_IMAGE_READ_MS`, the time a pass counts
|
|
76
|
+
* for it; anywhere else, and wherever a caller hands its own reads (a spec,
|
|
77
|
+
* the eval harness), they run here. Either way an answer that does not come
|
|
78
|
+
* is an unreadable photo.
|
|
72
79
|
*/ export async function readAiProductImage(firestore, input, seams = {}) {
|
|
73
80
|
if (!input.value) return {
|
|
74
81
|
status: 'none'
|
|
@@ -78,27 +85,18 @@
|
|
|
78
85
|
status: 'not-in-library'
|
|
79
86
|
};
|
|
80
87
|
try {
|
|
81
|
-
var
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
return {
|
|
95
|
-
status: 'read',
|
|
96
|
-
image: {
|
|
97
|
-
type: 'image',
|
|
98
|
-
mediaType: 'image/jpeg',
|
|
99
|
-
data: jpeg.toString('base64')
|
|
100
|
-
}
|
|
101
|
-
};
|
|
88
|
+
var _ref;
|
|
89
|
+
const images = seams.readBytes || seams.encode ? null : await import("./ai-image-reads.js");
|
|
90
|
+
const origin = (_ref = images == null ? void 0 : images.aiImageReadsOrigin()) != null ? _ref : null;
|
|
91
|
+
if (images && origin) {
|
|
92
|
+
const payload = images.aiImageReadsRequest('product-photo', location, input.hostId, input.orgId);
|
|
93
|
+
if (!payload) return {
|
|
94
|
+
status: 'unreadable'
|
|
95
|
+
};
|
|
96
|
+
const answer = await images.requestAiImageReads(origin, payload, AbortSignal.timeout(AI_PRODUCT_IMAGE_READ_MS));
|
|
97
|
+
return productImageAnswer(answer == null ? void 0 : answer.read);
|
|
98
|
+
}
|
|
99
|
+
return await readAiProductImageAt(firestore, location, input.hostId, seams);
|
|
102
100
|
} catch (error) {
|
|
103
101
|
console.error('ai product image read failed', {
|
|
104
102
|
hostId: input.hostId,
|
|
@@ -109,5 +107,59 @@
|
|
|
109
107
|
};
|
|
110
108
|
}
|
|
111
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The photo read and encoded in THIS process, from a location already held
|
|
112
|
+
* to the media library's rule: what `readAiProductImage` does off Vercel,
|
|
113
|
+
* and what the images function does for it on Vercel. Throws when the read
|
|
114
|
+
* or the encode does.
|
|
115
|
+
*/ export async function readAiProductImageAt(firestore, location, hostId, seams = {}) {
|
|
116
|
+
var _seams_readBytes, _seams_encode;
|
|
117
|
+
const read = (_seams_readBytes = seams.readBytes) != null ? _seams_readBytes : (store, where, site)=>readAiMediaAssetBytes(store, where, site, {
|
|
118
|
+
maxBytes: AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES,
|
|
119
|
+
types: AI_PRODUCT_IMAGE_TYPES
|
|
120
|
+
});
|
|
121
|
+
const asset = await read(firestore, location, hostId);
|
|
122
|
+
if (!asset) return {
|
|
123
|
+
status: 'unreadable'
|
|
124
|
+
};
|
|
125
|
+
const jpeg = await ((_seams_encode = seams.encode) != null ? _seams_encode : encodeAiProductImage)(asset.buffer);
|
|
126
|
+
if (!jpeg.length) return {
|
|
127
|
+
status: 'unreadable'
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
status: 'read',
|
|
131
|
+
image: {
|
|
132
|
+
type: 'image',
|
|
133
|
+
mediaType: 'image/jpeg',
|
|
134
|
+
data: jpeg.toString('base64')
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What the images function answered, held to the shapes a read takes: a
|
|
140
|
+
* JPEG picture part, or the asset not being the library's. Anything else is
|
|
141
|
+
* an unreadable photo.
|
|
142
|
+
*/ function productImageAnswer(answer) {
|
|
143
|
+
var _read_image, _read_image1;
|
|
144
|
+
const read = answer != null ? answer : {};
|
|
145
|
+
if (read.status === 'not-in-library') return {
|
|
146
|
+
status: 'not-in-library'
|
|
147
|
+
};
|
|
148
|
+
if (read.status !== 'read') return {
|
|
149
|
+
status: 'unreadable'
|
|
150
|
+
};
|
|
151
|
+
const data = (_read_image = read.image) == null ? void 0 : _read_image['data'];
|
|
152
|
+
const isJpeg = ((_read_image1 = read.image) == null ? void 0 : _read_image1['type']) === 'image' && read.image['mediaType'] === 'image/jpeg';
|
|
153
|
+
return isJpeg && typeof data === 'string' && data.length > 0 ? {
|
|
154
|
+
status: 'read',
|
|
155
|
+
image: {
|
|
156
|
+
type: 'image',
|
|
157
|
+
mediaType: 'image/jpeg',
|
|
158
|
+
data
|
|
159
|
+
}
|
|
160
|
+
} : {
|
|
161
|
+
status: 'unreadable'
|
|
162
|
+
};
|
|
163
|
+
}
|
|
112
164
|
|
|
113
165
|
//# sourceMappingURL=ai-product-image.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-product-image.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 { AiProductPhotoRead } from '../model/ai-products'\nimport type { AiImagePart } from '../providers/contract'\nimport {\n aiMediaAssetLocation,\n loadAiMediaSharp,\n readAiMediaAssetBytes,\n type AiMediaAssetBytes,\n type AiMediaAssetLocation,\n} from './ai-media-asset'\n\n/**\n * A PRODUCT'S PHOTO, as a `products` job shows it to the model (AGL-2916).\n *\n * Exactly one picture is read for a product: its FIRST media item, the photo\n * the storefront leads with. Nothing else of the media library is read or\n * sent — no other photo of the product, no other asset, no folder, no file\n * name and no alt text.\n *\n * The photo is read the way the media CDN would serve it\n * (`readAiMediaAssetBytes`): only an asset of this site's own library or its\n * org's, live, not private, visible to the site, not locked or quarantined.\n * A photo that is a link to somewhere else, or a raw storage URL, is not an\n * asset of the library and is not read at all; nothing is ever fetched from a\n * URL.\n *\n * What is sent is a new JPEG made from it: turned upright by its own\n * orientation, shrunk to fit inside `AI_PRODUCT_IMAGE_MAX_EDGE_PX` on its\n * longer edge (never enlarged), laid on white where it was transparent, and\n * encoded at `AI_PRODUCT_IMAGE_JPEG_QUALITY`. Encoding a new image carries none\n * of the original's metadata, so no camera, date or location embedded in the\n * file leaves with it. An animated image sends its first frame.\n */\n\n/** The longer edge of the picture sent, in pixels. */\nexport const AI_PRODUCT_IMAGE_MAX_EDGE_PX = 768\n\n/** The JPEG quality the picture is sent at. */\nexport const AI_PRODUCT_IMAGE_JPEG_QUALITY = 80\n\n/** The largest original read, in bytes. */\nexport const AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES = 15 * 1024 * 1024\n\n/** The formats read; a vector drawing is not a product photo, and is not decoded. */\nexport const AI_PRODUCT_IMAGE_TYPES = /^image\\/(png|jpeg|webp|gif|avif)$/\n\n/**\n * ASSUMED: how long the photo read takes — the media document, the CDN's\n * block, the storage download and the resize — which a pass's least time\n * counts (AGL-3035).\n */\nexport const AI_PRODUCT_IMAGE_READ_MS = 6_000\n\n/** The part of `sharp` the photo read uses. */\ninterface SharpJpegPipeline {\n rotate(): SharpJpegPipeline\n resize(options: { width: number; height: number; fit: 'inside'; withoutEnlargement: true }): SharpJpegPipeline\n flatten(options: { background: { r: number; g: number; b: number } }): SharpJpegPipeline\n jpeg(options: { quality: number }): SharpJpegPipeline\n toBuffer(): Promise<Buffer>\n}\ntype SharpJpeg = (input: Buffer, options?: { limitInputPixels?: number; animated?: boolean }) => SharpJpegPipeline\n\n/** The reads that leave the process, replaceable where a spec needs to. */\nexport interface AiProductImageSeams {\n readBytes?: (\n firestore: FirebaseFirestore.Firestore,\n location: AiMediaAssetLocation,\n hostId: string,\n ) => Promise<AiMediaAssetBytes | null>\n encode?: (bytes: Buffer) => Promise<Buffer>\n}\n\nexport type AiProductImage =\n | { status: 'read'; image: AiImagePart }\n | { status: Exclude<AiProductPhotoRead, 'read' | 'model'> }\n\n/** The JPEG the model is shown, made from the original's bytes. */\nexport async function encodeAiProductImage(bytes: Buffer): Promise<Buffer> {\n const sharp = (await loadAiMediaSharp()) as SharpJpeg\n return sharp(bytes, { limitInputPixels: 64_000_000, animated: false })\n .rotate()\n .resize({\n width: AI_PRODUCT_IMAGE_MAX_EDGE_PX,\n height: AI_PRODUCT_IMAGE_MAX_EDGE_PX,\n fit: 'inside',\n withoutEnlargement: true,\n })\n // White, as channels: a transparent product shot reads on a white page.\n .flatten({ background: { r: 255, g: 255, b: 255 } })\n .jpeg({ quality: AI_PRODUCT_IMAGE_JPEG_QUALITY })\n .toBuffer()\n}\n\n/**\n * The product's first photo as a picture part, or what kept it from being\n * one. Never throws: a photo that cannot be read leaves the copy to be written\n * from the product's words, and says so.\n */\nexport async function readAiProductImage(\n firestore: FirebaseFirestore.Firestore,\n input: { value: string | null | undefined; hostId: string; orgId: string },\n seams: AiProductImageSeams = {},\n): Promise<AiProductImage> {\n if (!input.value) return { status: 'none' }\n const location = aiMediaAssetLocation(input.value, input.hostId, input.orgId)\n if (!location) return { status: 'not-in-library' }\n try {\n const read =\n seams.readBytes ??\n ((store, where, hostId) =>\n readAiMediaAssetBytes(store, where, hostId, {\n maxBytes: AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES,\n types: AI_PRODUCT_IMAGE_TYPES,\n }))\n const asset = await read(firestore, location, input.hostId)\n if (!asset) return { status: 'unreadable' }\n const jpeg = await (seams.encode ?? encodeAiProductImage)(asset.buffer)\n if (!jpeg.length) return { status: 'unreadable' }\n return { status: 'read', image: { type: 'image', mediaType: 'image/jpeg', data: jpeg.toString('base64') } }\n } catch (error) {\n console.error('ai product image read failed', { hostId: input.hostId, error })\n return { status: 'unreadable' }\n }\n}\n"],"names":["aiMediaAssetLocation","loadAiMediaSharp","readAiMediaAssetBytes","AI_PRODUCT_IMAGE_MAX_EDGE_PX","AI_PRODUCT_IMAGE_JPEG_QUALITY","AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES","AI_PRODUCT_IMAGE_TYPES","AI_PRODUCT_IMAGE_READ_MS","encodeAiProductImage","bytes","sharp","limitInputPixels","animated","rotate","resize","width","height","fit","withoutEnlargement","flatten","background","r","g","b","jpeg","quality","toBuffer","readAiProductImage","firestore","input","seams","value","status","location","hostId","orgId","read","readBytes","store","where","maxBytes","types","asset","encode","buffer","length","image","type","mediaType","data","toString","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,EACpBC,gBAAgB,EAChBC,qBAAqB,QAGhB,sBAAkB;AAEzB;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,oDAAoD,GACpD,OAAO,MAAMC,+BAA+B,IAAG;AAE/C,6CAA6C,GAC7C,OAAO,MAAMC,gCAAgC,GAAE;AAE/C,yCAAyC,GACzC,OAAO,MAAMC,oCAAoC,KAAK,OAAO,KAAI;AAEjE,mFAAmF,GACnF,OAAO,MAAMC,yBAAyB,oCAAmC;AAEzE;;;;CAIC,GACD,OAAO,MAAMC,2BAA2B,KAAK;AA0B7C,iEAAiE,GACjE,OAAO,eAAeC,qBAAqBC,KAAa;IACtD,MAAMC,QAAS,MAAMT;IACrB,OAAOS,MAAMD,OAAO;QAAEE,kBAAkB;QAAYC,UAAU;IAAM,GACjEC,MAAM,GACNC,MAAM,CAAC;QACNC,OAAOZ;QACPa,QAAQb;QACRc,KAAK;QACLC,oBAAoB;IACtB,EACA,wEAAwE;KACvEC,OAAO,CAAC;QAAEC,YAAY;YAAEC,GAAG;YAAKC,GAAG;YAAKC,GAAG;QAAI;IAAE,GACjDC,IAAI,CAAC;QAAEC,SAASrB;IAA8B,GAC9CsB,QAAQ;AACb;AAEA;;;;CAIC,GACD,OAAO,eAAeC,mBACpBC,SAAsC,EACtCC,KAA0E,EAC1EC,QAA6B,CAAC,CAAC;IAE/B,IAAI,CAACD,MAAME,KAAK,EAAE,OAAO;QAAEC,QAAQ;IAAO;IAC1C,MAAMC,WAAWjC,qBAAqB6B,MAAME,KAAK,EAAEF,MAAMK,MAAM,EAAEL,MAAMM,KAAK;IAC5E,IAAI,CAACF,UAAU,OAAO;QAAED,QAAQ;IAAiB;IACjD,IAAI;YAEAF,kBAQkBA;QATpB,MAAMM,QACJN,mBAAAA,MAAMO,SAAS,YAAfP,mBACC,CAACQ,OAAOC,OAAOL,SACdhC,sBAAsBoC,OAAOC,OAAOL,QAAQ;gBAC1CM,UAAUnC;gBACVoC,OAAOnC;YACT;QACJ,MAAMoC,QAAQ,MAAMN,KAAKR,WAAWK,UAAUJ,MAAMK,MAAM;QAC1D,IAAI,CAACQ,OAAO,OAAO;YAAEV,QAAQ;QAAa;QAC1C,MAAMR,OAAO,MAAM,EAACM,gBAAAA,MAAMa,MAAM,YAAZb,gBAAgBtB,sBAAsBkC,MAAME,MAAM;QACtE,IAAI,CAACpB,KAAKqB,MAAM,EAAE,OAAO;YAAEb,QAAQ;QAAa;QAChD,OAAO;YAAEA,QAAQ;YAAQc,OAAO;gBAAEC,MAAM;gBAASC,WAAW;gBAAcC,MAAMzB,KAAK0B,QAAQ,CAAC;YAAU;QAAE;IAC5G,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAAC,gCAAgC;YAAEjB,QAAQL,MAAMK,MAAM;YAAEiB;QAAM;QAC5E,OAAO;YAAEnB,QAAQ;QAAa;IAChC;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-product-image.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 { AiProductPhotoRead } from '../model/ai-products'\nimport type { AiImagePart } from '../providers/contract'\nimport {\n aiMediaAssetLocation,\n loadAiMediaSharp,\n readAiMediaAssetBytes,\n type AiMediaAssetBytes,\n type AiMediaAssetLocation,\n} from './ai-media-asset'\n\n/**\n * A PRODUCT'S PHOTO, as a `products` job shows it to the model (AGL-2916).\n *\n * Exactly one picture is read for a product: its FIRST media item, the photo\n * the storefront leads with. Nothing else of the media library is read or\n * sent — no other photo of the product, no other asset, no folder, no file\n * name and no alt text.\n *\n * The photo is read the way the media CDN would serve it\n * (`readAiMediaAssetBytes`): only an asset of this site's own library or its\n * org's, live, not private, visible to the site, not locked or quarantined.\n * A photo that is a link to somewhere else, or a raw storage URL, is not an\n * asset of the library and is not read at all; nothing is ever fetched from a\n * URL.\n *\n * What is sent is a new JPEG made from it: turned upright by its own\n * orientation, shrunk to fit inside `AI_PRODUCT_IMAGE_MAX_EDGE_PX` on its\n * longer edge (never enlarged), laid on white where it was transparent, and\n * encoded at `AI_PRODUCT_IMAGE_JPEG_QUALITY`. Encoding a new image carries none\n * of the original's metadata, so no camera, date or location embedded in the\n * file leaves with it. An animated image sends its first frame.\n */\n\n/** The longer edge of the picture sent, in pixels. */\nexport const AI_PRODUCT_IMAGE_MAX_EDGE_PX = 768\n\n/** The JPEG quality the picture is sent at. */\nexport const AI_PRODUCT_IMAGE_JPEG_QUALITY = 80\n\n/** The largest original read, in bytes. */\nexport const AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES = 15 * 1024 * 1024\n\n/** The formats read; a vector drawing is not a product photo, and is not decoded. */\nexport const AI_PRODUCT_IMAGE_TYPES = /^image\\/(png|jpeg|webp|gif|avif)$/\n\n/**\n * ASSUMED: how long the photo read takes — the media document, the CDN's\n * block, the storage download and the resize — which a pass's least time\n * counts (AGL-3035).\n */\nexport const AI_PRODUCT_IMAGE_READ_MS = 6_000\n\n/** The part of `sharp` the photo read uses. */\ninterface SharpJpegPipeline {\n rotate(): SharpJpegPipeline\n resize(options: { width: number; height: number; fit: 'inside'; withoutEnlargement: true }): SharpJpegPipeline\n flatten(options: { background: { r: number; g: number; b: number } }): SharpJpegPipeline\n jpeg(options: { quality: number }): SharpJpegPipeline\n toBuffer(): Promise<Buffer>\n}\ntype SharpJpeg = (input: Buffer, options?: { limitInputPixels?: number; animated?: boolean }) => SharpJpegPipeline\n\n/** The reads that leave the process, replaceable where a spec needs to. */\nexport interface AiProductImageSeams {\n readBytes?: (\n firestore: FirebaseFirestore.Firestore,\n location: AiMediaAssetLocation,\n hostId: string,\n ) => Promise<AiMediaAssetBytes | null>\n encode?: (bytes: Buffer) => Promise<Buffer>\n}\n\nexport type AiProductImage =\n | { status: 'read'; image: AiImagePart }\n | { status: Exclude<AiProductPhotoRead, 'read' | 'model'> }\n\n/** The JPEG the model is shown, made from the original's bytes. */\nexport async function encodeAiProductImage(bytes: Buffer): Promise<Buffer> {\n const sharp = (await loadAiMediaSharp()) as SharpJpeg\n return sharp(bytes, { limitInputPixels: 64_000_000, animated: false })\n .rotate()\n .resize({\n width: AI_PRODUCT_IMAGE_MAX_EDGE_PX,\n height: AI_PRODUCT_IMAGE_MAX_EDGE_PX,\n fit: 'inside',\n withoutEnlargement: true,\n })\n // White, as channels: a transparent product shot reads on a white page.\n .flatten({ background: { r: 255, g: 255, b: 255 } })\n .jpeg({ quality: AI_PRODUCT_IMAGE_JPEG_QUALITY })\n .toBuffer()\n}\n\n/**\n * The product's first photo as a picture part, or what kept it from being\n * one. Never throws: a photo that cannot be read leaves the copy to be written\n * from the product's words, and says so.\n *\n * On Vercel the read and the encode run in the plugins' image function, the\n * one function that ships `sharp`'s native library (`ai-image-reads.ts`,\n * AGL-4048), bounded by `AI_PRODUCT_IMAGE_READ_MS`, the time a pass counts\n * for it; anywhere else, and wherever a caller hands its own reads (a spec,\n * the eval harness), they run here. Either way an answer that does not come\n * is an unreadable photo.\n */\nexport async function readAiProductImage(\n firestore: FirebaseFirestore.Firestore,\n input: { value: string | null | undefined; hostId: string; orgId: string },\n seams: AiProductImageSeams = {},\n): Promise<AiProductImage> {\n if (!input.value) return { status: 'none' }\n const location = aiMediaAssetLocation(input.value, input.hostId, input.orgId)\n if (!location) return { status: 'not-in-library' }\n try {\n const images = seams.readBytes || seams.encode ? null : await import('./ai-image-reads')\n const origin = images?.aiImageReadsOrigin() ?? null\n if (images && origin) {\n const payload = images.aiImageReadsRequest('product-photo', location, input.hostId, input.orgId)\n if (!payload) return { status: 'unreadable' }\n const answer = await images.requestAiImageReads<{ read?: unknown }>(\n origin,\n payload,\n AbortSignal.timeout(AI_PRODUCT_IMAGE_READ_MS),\n )\n return productImageAnswer(answer?.read)\n }\n return await readAiProductImageAt(firestore, location, input.hostId, seams)\n } catch (error) {\n console.error('ai product image read failed', { hostId: input.hostId, error })\n return { status: 'unreadable' }\n }\n}\n\n/**\n * The photo read and encoded in THIS process, from a location already held\n * to the media library's rule: what `readAiProductImage` does off Vercel,\n * and what the images function does for it on Vercel. Throws when the read\n * or the encode does.\n */\nexport async function readAiProductImageAt(\n firestore: FirebaseFirestore.Firestore,\n location: AiMediaAssetLocation,\n hostId: string,\n seams: AiProductImageSeams = {},\n): Promise<AiProductImage> {\n const read =\n seams.readBytes ??\n ((store, where, site) =>\n readAiMediaAssetBytes(store, where, site, {\n maxBytes: AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES,\n types: AI_PRODUCT_IMAGE_TYPES,\n }))\n const asset = await read(firestore, location, hostId)\n if (!asset) return { status: 'unreadable' }\n const jpeg = await (seams.encode ?? encodeAiProductImage)(asset.buffer)\n if (!jpeg.length) return { status: 'unreadable' }\n return { status: 'read', image: { type: 'image', mediaType: 'image/jpeg', data: jpeg.toString('base64') } }\n}\n\n/**\n * What the images function answered, held to the shapes a read takes: a\n * JPEG picture part, or the asset not being the library's. Anything else is\n * an unreadable photo.\n */\nfunction productImageAnswer(answer: unknown): AiProductImage {\n const read = (answer ?? {}) as { status?: unknown; image?: Record<string, unknown> | null }\n if (read.status === 'not-in-library') return { status: 'not-in-library' }\n if (read.status !== 'read') return { status: 'unreadable' }\n const data = read.image?.['data']\n const isJpeg = read.image?.['type'] === 'image' && read.image['mediaType'] === 'image/jpeg'\n return isJpeg && typeof data === 'string' && data.length > 0\n ? { status: 'read', image: { type: 'image', mediaType: 'image/jpeg', data } }\n : { status: 'unreadable' }\n}\n"],"names":["aiMediaAssetLocation","loadAiMediaSharp","readAiMediaAssetBytes","AI_PRODUCT_IMAGE_MAX_EDGE_PX","AI_PRODUCT_IMAGE_JPEG_QUALITY","AI_PRODUCT_IMAGE_MAX_SOURCE_BYTES","AI_PRODUCT_IMAGE_TYPES","AI_PRODUCT_IMAGE_READ_MS","encodeAiProductImage","bytes","sharp","limitInputPixels","animated","rotate","resize","width","height","fit","withoutEnlargement","flatten","background","r","g","b","jpeg","quality","toBuffer","readAiProductImage","firestore","input","seams","value","status","location","hostId","orgId","images","readBytes","encode","origin","aiImageReadsOrigin","payload","aiImageReadsRequest","answer","requestAiImageReads","AbortSignal","timeout","productImageAnswer","read","readAiProductImageAt","error","console","store","where","site","maxBytes","types","asset","buffer","length","image","type","mediaType","data","toString","isJpeg"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SACEA,oBAAoB,EACpBC,gBAAgB,EAChBC,qBAAqB,QAGhB,sBAAkB;AAEzB;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,oDAAoD,GACpD,OAAO,MAAMC,+BAA+B,IAAG;AAE/C,6CAA6C,GAC7C,OAAO,MAAMC,gCAAgC,GAAE;AAE/C,yCAAyC,GACzC,OAAO,MAAMC,oCAAoC,KAAK,OAAO,KAAI;AAEjE,mFAAmF,GACnF,OAAO,MAAMC,yBAAyB,oCAAmC;AAEzE;;;;CAIC,GACD,OAAO,MAAMC,2BAA2B,KAAK;AA0B7C,iEAAiE,GACjE,OAAO,eAAeC,qBAAqBC,KAAa;IACtD,MAAMC,QAAS,MAAMT;IACrB,OAAOS,MAAMD,OAAO;QAAEE,kBAAkB;QAAYC,UAAU;IAAM,GACjEC,MAAM,GACNC,MAAM,CAAC;QACNC,OAAOZ;QACPa,QAAQb;QACRc,KAAK;QACLC,oBAAoB;IACtB,EACA,wEAAwE;KACvEC,OAAO,CAAC;QAAEC,YAAY;YAAEC,GAAG;YAAKC,GAAG;YAAKC,GAAG;QAAI;IAAE,GACjDC,IAAI,CAAC;QAAEC,SAASrB;IAA8B,GAC9CsB,QAAQ;AACb;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,eAAeC,mBACpBC,SAAsC,EACtCC,KAA0E,EAC1EC,QAA6B,CAAC,CAAC;IAE/B,IAAI,CAACD,MAAME,KAAK,EAAE,OAAO;QAAEC,QAAQ;IAAO;IAC1C,MAAMC,WAAWjC,qBAAqB6B,MAAME,KAAK,EAAEF,MAAMK,MAAM,EAAEL,MAAMM,KAAK;IAC5E,IAAI,CAACF,UAAU,OAAO;QAAED,QAAQ;IAAiB;IACjD,IAAI;;QACF,MAAMI,SAASN,MAAMO,SAAS,IAAIP,MAAMQ,MAAM,GAAG,OAAO,MAAM,MAAM,CAAC;QACrE,MAAMC,iBAASH,0BAAAA,OAAQI,kBAAkB,qBAAM;QAC/C,IAAIJ,UAAUG,QAAQ;YACpB,MAAME,UAAUL,OAAOM,mBAAmB,CAAC,iBAAiBT,UAAUJ,MAAMK,MAAM,EAAEL,MAAMM,KAAK;YAC/F,IAAI,CAACM,SAAS,OAAO;gBAAET,QAAQ;YAAa;YAC5C,MAAMW,SAAS,MAAMP,OAAOQ,mBAAmB,CAC7CL,QACAE,SACAI,YAAYC,OAAO,CAACvC;YAEtB,OAAOwC,mBAAmBJ,0BAAAA,OAAQK,IAAI;QACxC;QACA,OAAO,MAAMC,qBAAqBrB,WAAWK,UAAUJ,MAAMK,MAAM,EAAEJ;IACvE,EAAE,OAAOoB,OAAO;QACdC,QAAQD,KAAK,CAAC,gCAAgC;YAAEhB,QAAQL,MAAMK,MAAM;YAAEgB;QAAM;QAC5E,OAAO;YAAElB,QAAQ;QAAa;IAChC;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeiB,qBACpBrB,SAAsC,EACtCK,QAA8B,EAC9BC,MAAc,EACdJ,QAA6B,CAAC,CAAC;QAG7BA,kBAQkBA;IATpB,MAAMkB,QACJlB,mBAAAA,MAAMO,SAAS,YAAfP,mBACC,CAACsB,OAAOC,OAAOC,OACdpD,sBAAsBkD,OAAOC,OAAOC,MAAM;YACxCC,UAAUlD;YACVmD,OAAOlD;QACT;IACJ,MAAMmD,QAAQ,MAAMT,KAAKpB,WAAWK,UAAUC;IAC9C,IAAI,CAACuB,OAAO,OAAO;QAAEzB,QAAQ;IAAa;IAC1C,MAAMR,OAAO,MAAM,EAACM,gBAAAA,MAAMQ,MAAM,YAAZR,gBAAgBtB,sBAAsBiD,MAAMC,MAAM;IACtE,IAAI,CAAClC,KAAKmC,MAAM,EAAE,OAAO;QAAE3B,QAAQ;IAAa;IAChD,OAAO;QAAEA,QAAQ;QAAQ4B,OAAO;YAAEC,MAAM;YAASC,WAAW;YAAcC,MAAMvC,KAAKwC,QAAQ,CAAC;QAAU;IAAE;AAC5G;AAEA;;;;CAIC,GACD,SAASjB,mBAAmBJ,MAAe;QAI5BK,aACEA;IAJf,MAAMA,OAAQL,iBAAAA,SAAU,CAAC;IACzB,IAAIK,KAAKhB,MAAM,KAAK,kBAAkB,OAAO;QAAEA,QAAQ;IAAiB;IACxE,IAAIgB,KAAKhB,MAAM,KAAK,QAAQ,OAAO;QAAEA,QAAQ;IAAa;IAC1D,MAAM+B,QAAOf,cAAAA,KAAKY,KAAK,qBAAVZ,WAAY,CAAC,OAAO;IACjC,MAAMiB,SAASjB,EAAAA,eAAAA,KAAKY,KAAK,qBAAVZ,YAAY,CAAC,OAAO,MAAK,WAAWA,KAAKY,KAAK,CAAC,YAAY,KAAK;IAC/E,OAAOK,UAAU,OAAOF,SAAS,YAAYA,KAAKJ,MAAM,GAAG,IACvD;QAAE3B,QAAQ;QAAQ4B,OAAO;YAAEC,MAAM;YAASC,WAAW;YAAcC;QAAK;IAAE,IAC1E;QAAE/B,QAAQ;IAAa;AAC7B"}
|
|
@@ -82,6 +82,12 @@ export declare const logoMediaLocation: typeof aiMediaAssetLocation;
|
|
|
82
82
|
* else is there.
|
|
83
83
|
*/
|
|
84
84
|
export declare function dominantColorsFromPixels(data: ArrayLike<number>, channels: number, count?: number): string[];
|
|
85
|
+
/**
|
|
86
|
+
* A library logo's colors read and decoded in THIS process, from a location
|
|
87
|
+
* already held to the media library's rule: what `readLogoColors` does off
|
|
88
|
+
* Vercel, and what the images function does for it on Vercel.
|
|
89
|
+
*/
|
|
90
|
+
export declare function readAiThemeLogoColorsAt(firestore: FirebaseFirestore.Firestore, location: AiThemeLogoLocation, hostId: string): Promise<string[] | null>;
|
|
85
91
|
/** The first https link in a brief, or `null` — an http link is not followed. */
|
|
86
92
|
export declare function referenceUrlOf(brief: string): URL | null;
|
|
87
93
|
/** How much a page's declared `theme-color` counts beside one use of a color. */
|
|
@@ -185,7 +191,9 @@ export declare function readReferencePage(url: URL, signal: AbortSignal): Promis
|
|
|
185
191
|
export declare function referencePageColors(page: Pick<AiReferencePage, 'html' | 'stylesheets'>): string[];
|
|
186
192
|
/** The two reads that leave the process, replaceable where a spec needs to. */
|
|
187
193
|
export interface AiThemeBrandSeams {
|
|
188
|
-
logoColors?: (firestore: FirebaseFirestore.Firestore, location: AiThemeLogoLocation, hostId: string
|
|
194
|
+
logoColors?: (firestore: FirebaseFirestore.Firestore, location: AiThemeLogoLocation, hostId: string,
|
|
195
|
+
/** The brand reads' bound, which a read made elsewhere waits no longer than. */
|
|
196
|
+
signal?: AbortSignal) => Promise<string[] | null>;
|
|
189
197
|
referenceColors?: (url: URL, signal: AbortSignal) => Promise<string[] | null>;
|
|
190
198
|
}
|
|
191
199
|
export declare function gatherAiThemeBrandInputs(input: {
|
|
@@ -134,7 +134,29 @@ const LOGO_TYPES = /^image\/(png|jpeg|webp|gif|avif|svg\+xml)$/;
|
|
|
134
134
|
* A library logo's colors, read the way the media CDN would serve it
|
|
135
135
|
* (`readAiMediaAssetBytes`). `null` when the asset may not be read or the bytes
|
|
136
136
|
* cannot be decoded.
|
|
137
|
-
|
|
137
|
+
*
|
|
138
|
+
* On Vercel the read and the decode run in the plugins' image function, the
|
|
139
|
+
* one function that ships `sharp`'s native library (`ai-image-reads.ts`,
|
|
140
|
+
* AGL-4048), inside the brand reads' own bound (`signal`); anywhere else they
|
|
141
|
+
* run here. Either way an answer that does not come is a logo whose colors
|
|
142
|
+
* could not be read.
|
|
143
|
+
*/ async function readLogoColors(firestore, location, hostId, signal) {
|
|
144
|
+
const images = await import("./ai-image-reads.js");
|
|
145
|
+
const origin = images.aiImageReadsOrigin();
|
|
146
|
+
if (!origin) return readAiThemeLogoColorsAt(firestore, location, hostId);
|
|
147
|
+
// The location was held to the site's org already: an org asset's scope
|
|
148
|
+
// IS that org, and a site's own asset needs none.
|
|
149
|
+
const orgId = location.scope.isOrg ? location.scope.scopeId : '';
|
|
150
|
+
const payload = images.aiImageReadsRequest('logo-colors', location, hostId, orgId);
|
|
151
|
+
if (!payload) return null;
|
|
152
|
+
const answer = await images.requestAiImageReads(origin, payload, signal != null ? signal : AbortSignal.timeout(AI_THEME_BRAND_BUDGET_MS));
|
|
153
|
+
return logoColorsAnswer(answer == null ? void 0 : answer.colors);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A library logo's colors read and decoded in THIS process, from a location
|
|
157
|
+
* already held to the media library's rule: what `readLogoColors` does off
|
|
158
|
+
* Vercel, and what the images function does for it on Vercel.
|
|
159
|
+
*/ export async function readAiThemeLogoColorsAt(firestore, location, hostId) {
|
|
138
160
|
const asset = await readAiMediaAssetBytes(firestore, location, hostId, {
|
|
139
161
|
maxBytes: AI_THEME_LOGO_MAX_BYTES,
|
|
140
162
|
types: LOGO_TYPES
|
|
@@ -150,6 +172,10 @@ const LOGO_TYPES = /^image\/(png|jpeg|webp|gif|avif|svg\+xml)$/;
|
|
|
150
172
|
});
|
|
151
173
|
return dominantColorsFromPixels(data, info.channels);
|
|
152
174
|
}
|
|
175
|
+
/** What the images function answered, as hex colors: only colors leave it. */ function logoColorsAnswer(colors) {
|
|
176
|
+
if (!Array.isArray(colors)) return null;
|
|
177
|
+
return colors.map((color)=>typeof color === 'string' ? normalizeThemeHex(color) : null).filter((hex)=>Boolean(hex)).slice(0, AI_THEME_BRAND_COLORS_PER_SOURCE);
|
|
178
|
+
}
|
|
153
179
|
/* ------------------------------------------------------------------------ *
|
|
154
180
|
* A page the brief links to
|
|
155
181
|
* ------------------------------------------------------------------------ */ const URL_IN_BRIEF = /\bhttps?:\/\/[^\s<>"'`)\]]+/i;
|
|
@@ -457,7 +483,7 @@ export async function gatherAiThemeBrandInputs(input, seams = {}) {
|
|
|
457
483
|
const location = logoMediaLocation(input.host['logoUrl'], input.hostId, String((_input_host_orgId = input.host['orgId']) != null ? _input_host_orgId : ''));
|
|
458
484
|
if (location) {
|
|
459
485
|
var _seams_logoColors;
|
|
460
|
-
const found = await ((_seams_logoColors = seams.logoColors) != null ? _seams_logoColors : readLogoColors)(input.firestore, location, input.hostId).catch(()=>null);
|
|
486
|
+
const found = await ((_seams_logoColors = seams.logoColors) != null ? _seams_logoColors : readLogoColors)(input.firestore, location, input.hostId, clock.signal).catch(()=>null);
|
|
461
487
|
if (found) add(found, 'logo');
|
|
462
488
|
else notes.push("The site logo's colors could not be read, so the proposal does not use them.");
|
|
463
489
|
}
|
|
@@ -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 (`'`), 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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"}
|
|
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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"}
|