@aglyn/aglyn 1.0.0-beta.282 → 1.0.0-beta.284

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.
@@ -0,0 +1,333 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { pluginMediaIngest } from "../../plugin-manager/plugin-media-ingest.js";
18
+ import { stockPhotoProvider, stockPhotoSourceKey } from "../../plugin-manager/stock-photo-provider.js";
19
+ import { normalizeStockPhotoQuery, STOCK_PHOTO_GONE_MESSAGE, stockPhotoHitRefOf, stockPhotoOrientationOf, stockPhotoPickerMediaOf, stockPhotoSafeLink, STOCK_PHOTOS_UNCONFIGURED_MESSAGE, stockPhotosBusyMessage, stockPhotoTargetOf } from "./stock-photo-picker.js";
20
+ /**
21
+ * The server half of the media picker's Stock photos tab (AGL-4066). The
22
+ * console's route verifies the caller and hands this the request body with
23
+ * two doors of its own: {@link StockPhotoPickerServerDeps.authorize}, the
24
+ * same scope check an upload makes (the member may add files to that
25
+ * library, the lockdown's uploads gate), and
26
+ * {@link StockPhotoPickerServerDeps.readMedia}, the stored document. The rest
27
+ * is here, against core's contracts alone:
28
+ *
29
+ * - `status` — whether a library is configured, and its name.
30
+ * - `search` — the provider's own search, its 24-hour cache and its rate
31
+ * limit in charge. A `null` answer (rate-limited, or the library down) is
32
+ * `busy`, a state the tab shows, never a 5xx: a library's bad minute is
33
+ * not a fault of the console's.
34
+ * - `preview` — a hit's small copy, as bytes, so no browser loads an
35
+ * address at the library.
36
+ * - `import` — the photo copied into the library through `core.media-ingest`
37
+ * as the member, which makes the upload's own checks (access, lockdown,
38
+ * structure, takedowns, plan and storage band). A copy the library already
39
+ * holds under the photo's source key is placed instead, never stored a
40
+ * second time. The credit rides the asset as the AI build writes it:
41
+ * `description` "Photo by … on …" and `stockPhoto` with the photographer,
42
+ * the photo's page and the license.
43
+ *
44
+ * A hit is found again by the search that found it, which the provider
45
+ * answers from its cache, so the addresses and the credit are always the
46
+ * provider's own and never what a browser sent.
47
+ */ /** The largest stored copy a photo may be. */ export const STOCK_PHOTO_IMPORT_MAX_BYTES = 10 * 1024 * 1024;
48
+ /** The largest preview handed to a browser. */ export const STOCK_PHOTO_PREVIEW_MAX_BYTES = 2 * 1024 * 1024;
49
+ /** How long a browser may keep a preview. */ const PREVIEW_CACHE_CONTROL = 'private, max-age=3600';
50
+ /** How long one process keeps a search's photos to find hits again in, before asking the provider. */ export const STOCK_PHOTO_HIT_MEMO_MS = 5 * 60 * 1000;
51
+ /** Searches one process keeps in that memo. */ const HIT_MEMO_SIZE = 50;
52
+ /** The tags a hit shows. */ const HIT_TAGS = 8;
53
+ /** The file extension a stored copy is named with, by type. */ const EXTENSIONS = {
54
+ 'image/jpeg': 'jpg',
55
+ 'image/png': 'png',
56
+ 'image/webp': 'webp',
57
+ 'image/gif': 'gif'
58
+ };
59
+ /** A memo of the last {@link HIT_MEMO_SIZE} searches, each kept {@link STOCK_PHOTO_HIT_MEMO_MS}. */ export function createStockPhotoHitMemo(now = Date.now) {
60
+ const held = new Map();
61
+ return {
62
+ get (key) {
63
+ const entry = held.get(key);
64
+ if (!entry) return null;
65
+ if (now() - entry.at > STOCK_PHOTO_HIT_MEMO_MS) {
66
+ held.delete(key);
67
+ return null;
68
+ }
69
+ return entry.photos;
70
+ },
71
+ set (key, photos) {
72
+ held.delete(key);
73
+ held.set(key, {
74
+ at: now(),
75
+ photos
76
+ });
77
+ while(held.size > HIT_MEMO_SIZE)held.delete(held.keys().next().value);
78
+ }
79
+ };
80
+ }
81
+ const processMemo = createStockPhotoHitMemo();
82
+ const json = (body, status = 200)=>Response.json(body, {
83
+ status,
84
+ headers: {
85
+ 'Cache-Control': 'no-store'
86
+ }
87
+ });
88
+ const refused = (status, message)=>json({
89
+ status: 'refused',
90
+ message
91
+ }, status);
92
+ /** The memo's key for a search. */ const memoKey = (provider, ref)=>`${provider.id}\n${ref.orientation}\n${ref.query.toLowerCase()}`;
93
+ /** A provider's hit as the tab shows it. */ export function stockPhotoPickerHitOf(provider, photo, search) {
94
+ var _photo_tags;
95
+ const pageUrl = stockPhotoSafeLink(photo.pageUrl);
96
+ if (!photo.id || !pageUrl || !(photo.width > 0) || !(photo.height > 0)) return null;
97
+ const credit = provider.credit(photo);
98
+ const licenseUrl = stockPhotoSafeLink(credit.licenseUrl);
99
+ const photographerUrl = stockPhotoSafeLink(photo.photographerUrl);
100
+ return _extends({
101
+ ref: {
102
+ query: search.query,
103
+ orientation: search.orientation,
104
+ id: photo.id
105
+ },
106
+ sourceKey: stockPhotoSourceKey(photo),
107
+ width: photo.width,
108
+ height: photo.height,
109
+ photographer: photo.photographer
110
+ }, photographerUrl ? {
111
+ photographerUrl
112
+ } : {}, {
113
+ pageUrl,
114
+ tags: ((_photo_tags = photo.tags) != null ? _photo_tags : []).slice(0, HIT_TAGS)
115
+ }, photo.alt ? {
116
+ alt: photo.alt
117
+ } : {}, {
118
+ credit: credit.text,
119
+ license: credit.license,
120
+ licenseUrl: licenseUrl != null ? licenseUrl : '',
121
+ attributionRequired: credit.attributionRequired
122
+ });
123
+ }
124
+ /**
125
+ * What the stored copy of `photo` is sent to the media library as: the
126
+ * shape the AI build's copies take, so every stock asset carries one credit.
127
+ */ export function stockPhotoPickerIngestRequest(input) {
128
+ var _target_hostId, _EXTENSIONS_bytes_contentType;
129
+ const { photo, bytes, target } = input;
130
+ const credit = input.provider.credit(photo);
131
+ const photographerUrl = stockPhotoSafeLink(photo.photographerUrl);
132
+ return _extends({
133
+ hostId: (_target_hostId = target.hostId) != null ? _target_hostId : ''
134
+ }, target.orgId ? {
135
+ orgId: target.orgId
136
+ } : {}, {
137
+ uid: input.uid,
138
+ fileName: `${photo.provider}-${photo.id}.${(_EXTENSIONS_bytes_contentType = EXTENSIONS[bytes.contentType]) != null ? _EXTENSIONS_bytes_contentType : 'jpg'}`,
139
+ contentType: bytes.contentType,
140
+ bytes: bytes.bytes
141
+ }, photo.alt ? {
142
+ alt: photo.alt
143
+ } : {}, {
144
+ description: credit.text
145
+ }, input.folderId ? {
146
+ folderId: input.folderId
147
+ } : {}, {
148
+ stockPhoto: _extends({
149
+ key: stockPhotoSourceKey(photo),
150
+ provider: photo.provider,
151
+ providerLabel: credit.providerLabel,
152
+ id: photo.id,
153
+ pageUrl: photo.pageUrl,
154
+ photographer: photo.photographer
155
+ }, photographerUrl ? {
156
+ photographerUrl
157
+ } : {}, {
158
+ license: credit.license,
159
+ licenseUrl: credit.licenseUrl,
160
+ attributionRequired: credit.attributionRequired,
161
+ query: input.query
162
+ })
163
+ });
164
+ }
165
+ /** The photo `ref` names, found again in its search; `null` when the provider cannot be asked, `false` when it is gone. */ async function findHit(provider, ref, memo, signal) {
166
+ var _photos_find;
167
+ const key = memoKey(provider, ref);
168
+ let photos = memo.get(key);
169
+ if (!photos) {
170
+ const answer = await provider.search({
171
+ query: ref.query,
172
+ orientation: ref.orientation
173
+ }, signal ? {
174
+ signal
175
+ } : undefined);
176
+ if (!answer) return null;
177
+ photos = answer.photos;
178
+ memo.set(key, photos);
179
+ }
180
+ return (_photos_find = photos.find((photo)=>photo.id === ref.id)) != null ? _photos_find : false;
181
+ }
182
+ /** Answers one request of the Stock photos tab. Never throws for the library's sake. */ export async function answerStockPhotoPickerRequest(body, deps) {
183
+ var _deps_provider, _deps_memo, _deps_ingest, _target_hostId;
184
+ const record = body != null ? body : {};
185
+ const action = record['action'];
186
+ if (action !== 'status' && action !== 'search' && action !== 'preview' && action !== 'import') {
187
+ return refused(400, 'Unknown request');
188
+ }
189
+ const target = stockPhotoTargetOf(record);
190
+ if (!target) return refused(400, 'Name the site or the workspace whose library to use');
191
+ const denied = await deps.authorize(target);
192
+ if (denied) return denied;
193
+ const provider = ((_deps_provider = deps.provider) != null ? _deps_provider : stockPhotoProvider)();
194
+ if (!provider) {
195
+ if (action === 'preview') return new Response(null, {
196
+ status: 204
197
+ });
198
+ return json({
199
+ status: 'unconfigured',
200
+ message: STOCK_PHOTOS_UNCONFIGURED_MESSAGE
201
+ });
202
+ }
203
+ const library = {
204
+ label: provider.label
205
+ };
206
+ const memo = (_deps_memo = deps.memo) != null ? _deps_memo : processMemo;
207
+ const signal = deps.signal;
208
+ if (action === 'status') return json({
209
+ status: 'ready',
210
+ library
211
+ });
212
+ if (action === 'search') {
213
+ const query = normalizeStockPhotoQuery(record['query']);
214
+ if (!query) return refused(400, 'Type a few words to search for, up to 100 characters');
215
+ const orientation = stockPhotoOrientationOf(record['orientation']);
216
+ let answer = null;
217
+ try {
218
+ answer = await provider.search({
219
+ query,
220
+ orientation
221
+ }, signal ? {
222
+ signal
223
+ } : undefined);
224
+ } catch (error) {
225
+ console.warn('stock photos: the search failed', {
226
+ provider: provider.id,
227
+ error: String(error).slice(0, 160)
228
+ });
229
+ }
230
+ if (!answer) return json({
231
+ status: 'busy',
232
+ message: stockPhotosBusyMessage(provider.label)
233
+ });
234
+ memo.set(memoKey(provider, {
235
+ query,
236
+ orientation
237
+ }), answer.photos);
238
+ const hits = answer.photos.map((photo)=>stockPhotoPickerHitOf(provider, photo, {
239
+ query,
240
+ orientation
241
+ })).filter((hit)=>hit !== null);
242
+ return json({
243
+ status: 'ok',
244
+ library,
245
+ query,
246
+ orientation,
247
+ hits
248
+ });
249
+ }
250
+ const ref = stockPhotoHitRefOf(record['ref']);
251
+ if (!ref) return refused(400, 'That photo could not be read. Search again and choose it from the results.');
252
+ let photo = null;
253
+ try {
254
+ photo = await findHit(provider, ref, memo, signal);
255
+ } catch (error) {
256
+ console.warn('stock photos: a search to find a photo again failed', {
257
+ provider: provider.id,
258
+ error: String(error).slice(0, 160)
259
+ });
260
+ }
261
+ if (action === 'preview') {
262
+ if (!photo || typeof provider.preview !== 'function') return new Response(null, {
263
+ status: 204
264
+ });
265
+ const small = await provider.preview(photo, _extends({
266
+ maxBytes: STOCK_PHOTO_PREVIEW_MAX_BYTES
267
+ }, signal ? {
268
+ signal
269
+ } : {})).catch(()=>null);
270
+ if (!small) return new Response(null, {
271
+ status: 204
272
+ });
273
+ return new Response(new Uint8Array(small.bytes), {
274
+ status: 200,
275
+ headers: {
276
+ 'Content-Type': small.contentType,
277
+ 'Cache-Control': PREVIEW_CACHE_CONTROL,
278
+ 'X-Content-Type-Options': 'nosniff'
279
+ }
280
+ });
281
+ }
282
+ // import
283
+ if (photo === null) return json({
284
+ status: 'busy',
285
+ message: stockPhotosBusyMessage(provider.label)
286
+ });
287
+ if (photo === false) return json({
288
+ status: 'gone',
289
+ message: STOCK_PHOTO_GONE_MESSAGE
290
+ }, 404);
291
+ const ingest = ((_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest)();
292
+ if (!ingest) return refused(503, 'The media library cannot take photos this way right now. Try again in a minute.');
293
+ const sourceKey = stockPhotoSourceKey(photo);
294
+ const placed = async (mediaId, reused)=>{
295
+ const data = await deps.readMedia(target, mediaId);
296
+ if (!data) return refused(500, 'The photo was stored but could not be read back. It is in the library; choose it there.');
297
+ return json({
298
+ status: 'ok',
299
+ reused,
300
+ media: stockPhotoPickerMediaOf(mediaId, data)
301
+ });
302
+ };
303
+ const held = await ingest.findStockPhoto(_extends({
304
+ hostId: (_target_hostId = target.hostId) != null ? _target_hostId : '',
305
+ sourceKey
306
+ }, target.orgId ? {
307
+ orgId: target.orgId
308
+ } : {}));
309
+ if (held) return placed(held.mediaId, true);
310
+ const bytes = await provider.download(photo, _extends({
311
+ maxBytes: STOCK_PHOTO_IMPORT_MAX_BYTES
312
+ }, signal ? {
313
+ signal
314
+ } : {})).catch(()=>null);
315
+ if (!bytes) return json({
316
+ status: 'busy',
317
+ message: stockPhotosBusyMessage(provider.label)
318
+ });
319
+ const folderId = typeof record['folderId'] === 'string' && record['folderId'] ? String(record['folderId']).slice(0, 64) : null;
320
+ const stored = await ingest.ingest(stockPhotoPickerIngestRequest({
321
+ provider,
322
+ target,
323
+ uid: deps.uid,
324
+ photo,
325
+ bytes,
326
+ query: ref.query,
327
+ folderId
328
+ }));
329
+ if (stored.ok === false) return refused(stored.status >= 400 ? stored.status : 500, stored.reason);
330
+ return placed(stored.mediaId, false);
331
+ }
332
+
333
+ //# sourceMappingURL=stock-photo-picker-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/app-utils/stock-photos/stock-photo-picker-server.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 {\n pluginMediaIngest,\n type PluginMediaIngest,\n type PluginMediaIngestRequest,\n} from '../../plugin-manager/plugin-media-ingest'\nimport {\n stockPhotoProvider,\n stockPhotoSourceKey,\n type StockPhoto,\n type StockPhotoProvider,\n} from '../../plugin-manager/stock-photo-provider'\nimport {\n normalizeStockPhotoQuery,\n STOCK_PHOTO_GONE_MESSAGE,\n stockPhotoHitRefOf,\n stockPhotoOrientationOf,\n stockPhotoPickerMediaOf,\n stockPhotoSafeLink,\n STOCK_PHOTOS_UNCONFIGURED_MESSAGE,\n stockPhotosBusyMessage,\n stockPhotoTargetOf,\n type StockPhotoHitRef,\n type StockPhotoImportAnswer,\n type StockPhotoPickerHit,\n type StockPhotoPickerTarget,\n type StockPhotoSearchAnswer,\n type StockPhotoStatusAnswer,\n} from './stock-photo-picker'\n\n/**\n * The server half of the media picker's Stock photos tab (AGL-4066). The\n * console's route verifies the caller and hands this the request body with\n * two doors of its own: {@link StockPhotoPickerServerDeps.authorize}, the\n * same scope check an upload makes (the member may add files to that\n * library, the lockdown's uploads gate), and\n * {@link StockPhotoPickerServerDeps.readMedia}, the stored document. The rest\n * is here, against core's contracts alone:\n *\n * - `status` — whether a library is configured, and its name.\n * - `search` — the provider's own search, its 24-hour cache and its rate\n * limit in charge. A `null` answer (rate-limited, or the library down) is\n * `busy`, a state the tab shows, never a 5xx: a library's bad minute is\n * not a fault of the console's.\n * - `preview` — a hit's small copy, as bytes, so no browser loads an\n * address at the library.\n * - `import` — the photo copied into the library through `core.media-ingest`\n * as the member, which makes the upload's own checks (access, lockdown,\n * structure, takedowns, plan and storage band). A copy the library already\n * holds under the photo's source key is placed instead, never stored a\n * second time. The credit rides the asset as the AI build writes it:\n * `description` \"Photo by … on …\" and `stockPhoto` with the photographer,\n * the photo's page and the license.\n *\n * A hit is found again by the search that found it, which the provider\n * answers from its cache, so the addresses and the credit are always the\n * provider's own and never what a browser sent.\n */\n\n/** The largest stored copy a photo may be. */\nexport const STOCK_PHOTO_IMPORT_MAX_BYTES = 10 * 1024 * 1024\n/** The largest preview handed to a browser. */\nexport const STOCK_PHOTO_PREVIEW_MAX_BYTES = 2 * 1024 * 1024\n/** How long a browser may keep a preview. */\nconst PREVIEW_CACHE_CONTROL = 'private, max-age=3600'\n/** How long one process keeps a search's photos to find hits again in, before asking the provider. */\nexport const STOCK_PHOTO_HIT_MEMO_MS = 5 * 60 * 1000\n/** Searches one process keeps in that memo. */\nconst HIT_MEMO_SIZE = 50\n/** The tags a hit shows. */\nconst HIT_TAGS = 8\n/** The file extension a stored copy is named with, by type. */\nconst EXTENSIONS: Readonly<Record<string, string>> = {\n 'image/jpeg': 'jpg',\n 'image/png': 'png',\n 'image/webp': 'webp',\n 'image/gif': 'gif',\n}\n\nexport interface StockPhotoPickerServerDeps {\n /** The signed-in member. */\n uid: string\n /**\n * `null` when the member may add files to `target`'s library; otherwise\n * the refusal to answer with (401, 403, 404, 423 …).\n */\n authorize(target: StockPhotoPickerTarget): Promise<Response | null>\n /** The stored media document `mediaId` in `target`'s library, or `null`. */\n readMedia(target: StockPhotoPickerTarget, mediaId: string): Promise<Record<string, unknown> | null>\n provider?: () => StockPhotoProvider | null\n ingest?: () => PluginMediaIngest | null\n memo?: StockPhotoHitMemo\n signal?: AbortSignal\n}\n\n/** A process's recent searches, to find a hit again without asking the provider. */\nexport interface StockPhotoHitMemo {\n get(key: string): StockPhoto[] | null\n set(key: string, photos: StockPhoto[]): void\n}\n\n/** A memo of the last {@link HIT_MEMO_SIZE} searches, each kept {@link STOCK_PHOTO_HIT_MEMO_MS}. */\nexport function createStockPhotoHitMemo(now: () => number = Date.now): StockPhotoHitMemo {\n const held = new Map<string, { at: number; photos: StockPhoto[] }>()\n return {\n get(key) {\n const entry = held.get(key)\n if (!entry) return null\n if (now() - entry.at > STOCK_PHOTO_HIT_MEMO_MS) {\n held.delete(key)\n return null\n }\n return entry.photos\n },\n set(key, photos) {\n held.delete(key)\n held.set(key, { at: now(), photos })\n while (held.size > HIT_MEMO_SIZE) held.delete(held.keys().next().value as string)\n },\n }\n}\n\nconst processMemo = createStockPhotoHitMemo()\n\nconst json = (body: unknown, status = 200): Response =>\n Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } })\n\nconst refused = (status: number, message: string): Response => json({ status: 'refused', message }, status)\n\n/** The memo's key for a search. */\nconst memoKey = (provider: StockPhotoProvider, ref: Pick<StockPhotoHitRef, 'query' | 'orientation'>) =>\n `${provider.id}\\n${ref.orientation}\\n${ref.query.toLowerCase()}`\n\n/** A provider's hit as the tab shows it. */\nexport function stockPhotoPickerHitOf(\n provider: Pick<StockPhotoProvider, 'credit'>,\n photo: StockPhoto,\n search: Pick<StockPhotoHitRef, 'query' | 'orientation'>,\n): StockPhotoPickerHit | null {\n const pageUrl = stockPhotoSafeLink(photo.pageUrl)\n if (!photo.id || !pageUrl || !(photo.width > 0) || !(photo.height > 0)) return null\n const credit = provider.credit(photo)\n const licenseUrl = stockPhotoSafeLink(credit.licenseUrl)\n const photographerUrl = stockPhotoSafeLink(photo.photographerUrl)\n return {\n ref: { query: search.query, orientation: search.orientation, id: photo.id },\n sourceKey: stockPhotoSourceKey(photo),\n width: photo.width,\n height: photo.height,\n photographer: photo.photographer,\n ...(photographerUrl ? { photographerUrl } : {}),\n pageUrl,\n tags: (photo.tags ?? []).slice(0, HIT_TAGS),\n ...(photo.alt ? { alt: photo.alt } : {}),\n credit: credit.text,\n license: credit.license,\n licenseUrl: licenseUrl ?? '',\n attributionRequired: credit.attributionRequired,\n }\n}\n\n/**\n * What the stored copy of `photo` is sent to the media library as: the\n * shape the AI build's copies take, so every stock asset carries one credit.\n */\nexport function stockPhotoPickerIngestRequest(input: {\n provider: Pick<StockPhotoProvider, 'credit'>\n target: StockPhotoPickerTarget\n uid: string\n photo: StockPhoto\n bytes: { bytes: Uint8Array; contentType: string }\n query: string\n folderId?: string | null\n}): PluginMediaIngestRequest {\n const { photo, bytes, target } = input\n const credit = input.provider.credit(photo)\n const photographerUrl = stockPhotoSafeLink(photo.photographerUrl)\n return {\n hostId: target.hostId ?? '',\n ...(target.orgId ? { orgId: target.orgId } : {}),\n uid: input.uid,\n fileName: `${photo.provider}-${photo.id}.${EXTENSIONS[bytes.contentType] ?? 'jpg'}`,\n contentType: bytes.contentType,\n bytes: bytes.bytes,\n ...(photo.alt ? { alt: photo.alt } : {}),\n description: credit.text,\n ...(input.folderId ? { folderId: input.folderId } : {}),\n stockPhoto: {\n key: stockPhotoSourceKey(photo),\n provider: photo.provider,\n providerLabel: credit.providerLabel,\n id: photo.id,\n pageUrl: photo.pageUrl,\n photographer: photo.photographer,\n ...(photographerUrl ? { photographerUrl } : {}),\n license: credit.license,\n licenseUrl: credit.licenseUrl,\n attributionRequired: credit.attributionRequired,\n query: input.query,\n },\n }\n}\n\n/** The photo `ref` names, found again in its search; `null` when the provider cannot be asked, `false` when it is gone. */\nasync function findHit(\n provider: StockPhotoProvider,\n ref: StockPhotoHitRef,\n memo: StockPhotoHitMemo,\n signal?: AbortSignal,\n): Promise<StockPhoto | null | false> {\n const key = memoKey(provider, ref)\n let photos = memo.get(key)\n if (!photos) {\n const answer = await provider.search({ query: ref.query, orientation: ref.orientation }, signal ? { signal } : undefined)\n if (!answer) return null\n photos = answer.photos\n memo.set(key, photos)\n }\n return photos.find((photo) => photo.id === ref.id) ?? false\n}\n\n/** Answers one request of the Stock photos tab. Never throws for the library's sake. */\nexport async function answerStockPhotoPickerRequest(\n body: unknown,\n deps: StockPhotoPickerServerDeps,\n): Promise<Response> {\n const record = (body ?? {}) as Record<string, unknown>\n const action = record['action']\n if (action !== 'status' && action !== 'search' && action !== 'preview' && action !== 'import') {\n return refused(400, 'Unknown request')\n }\n const target = stockPhotoTargetOf(record)\n if (!target) return refused(400, 'Name the site or the workspace whose library to use')\n const denied = await deps.authorize(target)\n if (denied) return denied\n\n const provider = (deps.provider ?? stockPhotoProvider)()\n if (!provider) {\n if (action === 'preview') return new Response(null, { status: 204 })\n return json({ status: 'unconfigured', message: STOCK_PHOTOS_UNCONFIGURED_MESSAGE } satisfies StockPhotoStatusAnswer)\n }\n const library = { label: provider.label }\n const memo = deps.memo ?? processMemo\n const signal = deps.signal\n\n if (action === 'status') return json({ status: 'ready', library } satisfies StockPhotoStatusAnswer)\n\n if (action === 'search') {\n const query = normalizeStockPhotoQuery(record['query'])\n if (!query) return refused(400, 'Type a few words to search for, up to 100 characters')\n const orientation = stockPhotoOrientationOf(record['orientation'])\n let answer: Awaited<ReturnType<StockPhotoProvider['search']>> = null\n try {\n answer = await provider.search({ query, orientation }, signal ? { signal } : undefined)\n } catch (error) {\n console.warn('stock photos: the search failed', { provider: provider.id, error: String(error).slice(0, 160) })\n }\n if (!answer) return json({ status: 'busy', message: stockPhotosBusyMessage(provider.label) } satisfies StockPhotoSearchAnswer)\n memo.set(memoKey(provider, { query, orientation }), answer.photos)\n const hits = answer.photos\n .map((photo) => stockPhotoPickerHitOf(provider, photo, { query, orientation }))\n .filter((hit): hit is StockPhotoPickerHit => hit !== null)\n return json({ status: 'ok', library, query, orientation, hits } satisfies StockPhotoSearchAnswer)\n }\n\n const ref = stockPhotoHitRefOf(record['ref'])\n if (!ref) return refused(400, 'That photo could not be read. Search again and choose it from the results.')\n let photo: StockPhoto | null | false = null\n try {\n photo = await findHit(provider, ref, memo, signal)\n } catch (error) {\n console.warn('stock photos: a search to find a photo again failed', { provider: provider.id, error: String(error).slice(0, 160) })\n }\n\n if (action === 'preview') {\n if (!photo || typeof provider.preview !== 'function') return new Response(null, { status: 204 })\n const small = await provider.preview(photo, { maxBytes: STOCK_PHOTO_PREVIEW_MAX_BYTES, ...(signal ? { signal } : {}) }).catch((): null => null)\n if (!small) return new Response(null, { status: 204 })\n return new Response(new Uint8Array(small.bytes), {\n status: 200,\n headers: { 'Content-Type': small.contentType, 'Cache-Control': PREVIEW_CACHE_CONTROL, 'X-Content-Type-Options': 'nosniff' },\n })\n }\n\n // import\n if (photo === null) return json({ status: 'busy', message: stockPhotosBusyMessage(provider.label) } satisfies StockPhotoImportAnswer)\n if (photo === false) return json({ status: 'gone', message: STOCK_PHOTO_GONE_MESSAGE } satisfies StockPhotoImportAnswer, 404)\n const ingest = (deps.ingest ?? pluginMediaIngest)()\n if (!ingest) return refused(503, 'The media library cannot take photos this way right now. Try again in a minute.')\n const sourceKey = stockPhotoSourceKey(photo)\n const placed = async (mediaId: string, reused: boolean): Promise<Response> => {\n const data = await deps.readMedia(target, mediaId)\n if (!data) return refused(500, 'The photo was stored but could not be read back. It is in the library; choose it there.')\n return json({ status: 'ok', reused, media: stockPhotoPickerMediaOf(mediaId, data) } satisfies StockPhotoImportAnswer)\n }\n const held = await ingest.findStockPhoto({\n hostId: target.hostId ?? '',\n sourceKey,\n ...(target.orgId ? { orgId: target.orgId } : {}),\n })\n if (held) return placed(held.mediaId, true)\n const bytes = await provider\n .download(photo, { maxBytes: STOCK_PHOTO_IMPORT_MAX_BYTES, ...(signal ? { signal } : {}) })\n .catch((): null => null)\n if (!bytes) return json({ status: 'busy', message: stockPhotosBusyMessage(provider.label) } satisfies StockPhotoImportAnswer)\n const folderId = typeof record['folderId'] === 'string' && record['folderId'] ? String(record['folderId']).slice(0, 64) : null\n const stored = await ingest.ingest(\n stockPhotoPickerIngestRequest({ provider, target, uid: deps.uid, photo, bytes, query: ref.query, folderId }),\n )\n if (stored.ok === false) return refused(stored.status >= 400 ? stored.status : 500, stored.reason)\n return placed(stored.mediaId, false)\n}\n"],"names":["pluginMediaIngest","stockPhotoProvider","stockPhotoSourceKey","normalizeStockPhotoQuery","STOCK_PHOTO_GONE_MESSAGE","stockPhotoHitRefOf","stockPhotoOrientationOf","stockPhotoPickerMediaOf","stockPhotoSafeLink","STOCK_PHOTOS_UNCONFIGURED_MESSAGE","stockPhotosBusyMessage","stockPhotoTargetOf","STOCK_PHOTO_IMPORT_MAX_BYTES","STOCK_PHOTO_PREVIEW_MAX_BYTES","PREVIEW_CACHE_CONTROL","STOCK_PHOTO_HIT_MEMO_MS","HIT_MEMO_SIZE","HIT_TAGS","EXTENSIONS","createStockPhotoHitMemo","now","Date","held","Map","get","key","entry","at","delete","photos","set","size","keys","next","value","processMemo","json","body","status","Response","headers","refused","message","memoKey","provider","ref","id","orientation","query","toLowerCase","stockPhotoPickerHitOf","photo","search","pageUrl","width","height","credit","licenseUrl","photographerUrl","sourceKey","photographer","tags","slice","alt","text","license","attributionRequired","stockPhotoPickerIngestRequest","input","target","bytes","hostId","orgId","uid","fileName","contentType","description","folderId","stockPhoto","providerLabel","findHit","memo","signal","answer","undefined","find","answerStockPhotoPickerRequest","deps","record","action","denied","authorize","library","label","error","console","warn","String","hits","map","filter","hit","preview","small","maxBytes","catch","Uint8Array","ingest","placed","mediaId","reused","data","readMedia","media","findStockPhoto","download","stored","ok","reason"],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type StockPhotoOrientation } from '../../plugin-manager/stock-photo-provider';
18
+ /**
19
+ * STOCK PHOTOS IN THE MEDIA PICKER (AGL-4066): the wire between the
20
+ * console's media picker and its server route, `/api/media/stock-photos`.
21
+ *
22
+ * A person editing a site by hand searches the deployment's stock photo
23
+ * library from the picker's Stock photos tab and places one. The route asks
24
+ * core's `core.stock-photos` provider and copies the chosen photo into the
25
+ * library the picker is filing into through `core.media-ingest`, as the
26
+ * signed-in member; the placed image is always that copy, never an address
27
+ * at the library. This module is the shapes both sides agree on, the checks
28
+ * the server makes on what a browser sends, and the browser's client. It
29
+ * names no library: the provider's own label comes back in every answer.
30
+ *
31
+ * Every request names one library, the target: a site's (`hostId`) or the
32
+ * workspace's shared one (`orgId`). A hit is named back to the server by
33
+ * {@link StockPhotoHitRef} — the search that found it and its id — never by
34
+ * the addresses or the credit, so the server finds the photo again in the
35
+ * provider's own cached answer and nothing a browser claims about a photo
36
+ * is stored.
37
+ */
38
+ /** The console route the picker's Stock photos tab talks to. */
39
+ export declare const STOCK_PHOTO_PICKER_ROUTE = "/api/media/stock-photos";
40
+ /** The library a request searches for and copies into. */
41
+ export type StockPhotoPickerTarget = {
42
+ hostId: string;
43
+ orgId?: undefined;
44
+ } | {
45
+ orgId: string;
46
+ hostId?: undefined;
47
+ };
48
+ /** The orientations the tab offers, in its order. */
49
+ export declare const STOCK_PHOTO_PICKER_ORIENTATIONS: readonly StockPhotoOrientation[];
50
+ /** How a search result names its photo back to the server. */
51
+ export interface StockPhotoHitRef {
52
+ /** The search words exactly as searched, after {@link normalizeStockPhotoQuery}. */
53
+ query: string;
54
+ orientation: StockPhotoOrientation;
55
+ /** The library's own id for the photo. */
56
+ id: string;
57
+ }
58
+ /** One photo a search found, as the tab shows it. */
59
+ export interface StockPhotoPickerHit {
60
+ ref: StockPhotoHitRef;
61
+ /** `{provider}:{id}`: what the library reuses an earlier copy by. */
62
+ sourceKey: string;
63
+ /** The size of the copy that is stored when the photo is used. */
64
+ width: number;
65
+ height: number;
66
+ photographer: string;
67
+ photographerUrl?: string;
68
+ /** The photo's page at the library. */
69
+ pageUrl: string;
70
+ /** The library's tags, a few, lowercase. */
71
+ tags: string[];
72
+ /** The library's own description of the photo, where it writes one. */
73
+ alt?: string;
74
+ /** The sentence the stored copy's description carries: "Photo by … on …". */
75
+ credit: string;
76
+ license: string;
77
+ licenseUrl: string;
78
+ attributionRequired: boolean;
79
+ }
80
+ /** The library the deployment searches, by the name its terms ask for. */
81
+ export interface StockPhotoPickerLibrary {
82
+ label: string;
83
+ }
84
+ /** What the tab is told when it opens. */
85
+ export type StockPhotoStatusAnswer = {
86
+ status: 'ready';
87
+ library: StockPhotoPickerLibrary;
88
+ } | {
89
+ status: 'unconfigured';
90
+ message: string;
91
+ } | {
92
+ status: 'refused';
93
+ message: string;
94
+ };
95
+ /** What a search answers. `busy`: the library could not be asked just now. */
96
+ export type StockPhotoSearchAnswer = {
97
+ status: 'ok';
98
+ library: StockPhotoPickerLibrary;
99
+ query: string;
100
+ orientation: StockPhotoOrientation;
101
+ hits: StockPhotoPickerHit[];
102
+ } | {
103
+ status: 'busy';
104
+ message: string;
105
+ } | {
106
+ status: 'unconfigured';
107
+ message: string;
108
+ } | {
109
+ status: 'refused';
110
+ message: string;
111
+ };
112
+ /**
113
+ * A stored asset as the picker places it: the media document's own fields,
114
+ * the ones a library pick hands on.
115
+ */
116
+ export interface StockPhotoPickerMedia {
117
+ $id: string;
118
+ fileName?: string;
119
+ contentType?: string;
120
+ sizeBytes?: number;
121
+ url?: string;
122
+ cdnPath?: string;
123
+ folderId?: string | null;
124
+ alt?: string;
125
+ description?: string;
126
+ width?: number;
127
+ height?: number;
128
+ variants?: number[];
129
+ visibleTo?: string[];
130
+ }
131
+ /** What using a photo answers. `reused`: the library already held a copy, which is placed. */
132
+ export type StockPhotoImportAnswer = {
133
+ status: 'ok';
134
+ reused: boolean;
135
+ media: StockPhotoPickerMedia;
136
+ } | {
137
+ status: 'busy';
138
+ message: string;
139
+ } | {
140
+ status: 'gone';
141
+ message: string;
142
+ } | {
143
+ status: 'unconfigured';
144
+ message: string;
145
+ } | {
146
+ status: 'refused';
147
+ message: string;
148
+ };
149
+ /** Said when the deployment has no stock photo library. */
150
+ export declare const STOCK_PHOTOS_UNCONFIGURED_MESSAGE: string;
151
+ /** Said when the library could not be asked, or could not send a photo, just now. */
152
+ export declare function stockPhotosBusyMessage(label: string): string;
153
+ /** Said when the photo is no longer among the search's results. */
154
+ export declare const STOCK_PHOTO_GONE_MESSAGE = "That photo is no longer in the search results. Search again and choose it from the new results.";
155
+ /**
156
+ * The search words as searched: control characters dropped, runs of space
157
+ * made one, trimmed. `null` when nothing is left or it is longer than
158
+ * {@link STOCK_PHOTO_QUERY_MAX_CHARS}.
159
+ */
160
+ export declare function normalizeStockPhotoQuery(value: unknown): string | null;
161
+ /** An orientation the tab offers, `any` for anything else. */
162
+ export declare function stockPhotoOrientationOf(value: unknown): StockPhotoOrientation;
163
+ /** The library a request names, or `null` when it names none, or both. */
164
+ export declare function stockPhotoTargetOf(body: unknown): StockPhotoPickerTarget | null;
165
+ /** The photo a request names, or `null` when it is not one a search could have found. */
166
+ export declare function stockPhotoHitRefOf(value: unknown): StockPhotoHitRef | null;
167
+ /** `value` when it is an https address, else `undefined`: nothing else is ever linked. */
168
+ export declare function stockPhotoSafeLink(value: unknown): string | undefined;
169
+ /** The fields of a stored media document the picker hands on, from the document. */
170
+ export declare function stockPhotoPickerMediaOf(mediaId: string, data: Record<string, unknown>): StockPhotoPickerMedia;
171
+ /** Posts one JSON request to the route, with whatever credentials the caller adds. */
172
+ export type StockPhotoPickerPost = (body: Record<string, unknown>) => Promise<Response>;
173
+ /** The browser's side of the route, for one library. */
174
+ export interface StockPhotoPickerClient {
175
+ status(): Promise<StockPhotoStatusAnswer>;
176
+ search(query: string, orientation: StockPhotoOrientation): Promise<StockPhotoSearchAnswer>;
177
+ /** The hit's small copy, or `null` when none can be had. */
178
+ preview(ref: StockPhotoHitRef): Promise<Blob | null>;
179
+ /** Copies the photo into the library, or finds the copy it holds. */
180
+ importPhoto(ref: StockPhotoHitRef, options?: {
181
+ folderId?: string | null;
182
+ }): Promise<StockPhotoImportAnswer>;
183
+ }
184
+ /**
185
+ * The client for one library. `post` carries the member's credentials: the
186
+ * console hands in `authorizedFetch` bound to the route.
187
+ */
188
+ export declare function createStockPhotoPickerClient(post: StockPhotoPickerPost, target: StockPhotoPickerTarget): StockPhotoPickerClient;