@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.
- package/package.json +11 -11
- package/src/lib/app-utils/docs-index.generated.js +2 -2
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +19 -0
- package/src/lib/app-utils/error-beacon.js +28 -0
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.d.ts +95 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.js +333 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.js.map +1 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.d.ts +188 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.js +193 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.js.map +1 -0
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +15 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +12 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
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 { STOCK_PHOTO_QUERY_MAX_CHARS } from "../../plugin-manager/stock-photo-provider.js";
|
|
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
|
+
*/ /** The console route the picker's Stock photos tab talks to. */ export const STOCK_PHOTO_PICKER_ROUTE = '/api/media/stock-photos';
|
|
38
|
+
/** The orientations the tab offers, in its order. */ export const STOCK_PHOTO_PICKER_ORIENTATIONS = [
|
|
39
|
+
'any',
|
|
40
|
+
'horizontal',
|
|
41
|
+
'vertical'
|
|
42
|
+
];
|
|
43
|
+
/** Said when the deployment has no stock photo library. */ export const STOCK_PHOTOS_UNCONFIGURED_MESSAGE = 'Stock photos are not set up here, so there is no library to search. ' + 'You can still upload your own photos in the library tabs.';
|
|
44
|
+
/** Said when the library could not be asked, or could not send a photo, just now. */ export function stockPhotosBusyMessage(label) {
|
|
45
|
+
return `${label || 'The stock photo library'} is busy right now. Try again in a minute.`;
|
|
46
|
+
}
|
|
47
|
+
/** Said when the photo is no longer among the search's results. */ export const STOCK_PHOTO_GONE_MESSAGE = 'That photo is no longer in the search results. Search again and choose it from the new results.';
|
|
48
|
+
/** The longest id a library may give a photo here. */ const MAX_ID_CHARS = 64;
|
|
49
|
+
/** A library id: a site's or a workspace's document id, never a path. */ const isDocumentId = (value)=>typeof value === 'string' && value.length > 0 && value.length <= 128 && !value.includes('/') && value.trim() === value;
|
|
50
|
+
/**
|
|
51
|
+
* The search words as searched: control characters dropped, runs of space
|
|
52
|
+
* made one, trimmed. `null` when nothing is left or it is longer than
|
|
53
|
+
* {@link STOCK_PHOTO_QUERY_MAX_CHARS}.
|
|
54
|
+
*/ export function normalizeStockPhotoQuery(value) {
|
|
55
|
+
if (typeof value !== 'string') return null;
|
|
56
|
+
// eslint-disable-next-line no-control-regex
|
|
57
|
+
const query = value.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
58
|
+
if (!query || query.length > STOCK_PHOTO_QUERY_MAX_CHARS) return null;
|
|
59
|
+
return query;
|
|
60
|
+
}
|
|
61
|
+
/** An orientation the tab offers, `any` for anything else. */ export function stockPhotoOrientationOf(value) {
|
|
62
|
+
return STOCK_PHOTO_PICKER_ORIENTATIONS.includes(value) ? value : 'any';
|
|
63
|
+
}
|
|
64
|
+
/** The library a request names, or `null` when it names none, or both. */ export function stockPhotoTargetOf(body) {
|
|
65
|
+
const record = body != null ? body : {};
|
|
66
|
+
const hostId = record['hostId'];
|
|
67
|
+
const orgId = record['orgId'];
|
|
68
|
+
const hasHost = hostId !== undefined && hostId !== null && hostId !== '';
|
|
69
|
+
const hasOrg = orgId !== undefined && orgId !== null && orgId !== '';
|
|
70
|
+
if (hasHost === hasOrg) return null;
|
|
71
|
+
if (hasHost) return isDocumentId(hostId) ? {
|
|
72
|
+
hostId
|
|
73
|
+
} : null;
|
|
74
|
+
return isDocumentId(orgId) ? {
|
|
75
|
+
orgId
|
|
76
|
+
} : null;
|
|
77
|
+
}
|
|
78
|
+
/** The photo a request names, or `null` when it is not one a search could have found. */ export function stockPhotoHitRefOf(value) {
|
|
79
|
+
const record = value != null ? value : {};
|
|
80
|
+
const query = normalizeStockPhotoQuery(record['query']);
|
|
81
|
+
const id = record['id'];
|
|
82
|
+
if (!query || typeof id !== 'string' || !id || id.length > MAX_ID_CHARS || !/^[\w.-]+$/.test(id)) return null;
|
|
83
|
+
return {
|
|
84
|
+
query,
|
|
85
|
+
orientation: stockPhotoOrientationOf(record['orientation']),
|
|
86
|
+
id
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** `value` when it is an https address, else `undefined`: nothing else is ever linked. */ export function stockPhotoSafeLink(value) {
|
|
90
|
+
if (typeof value !== 'string') return undefined;
|
|
91
|
+
try {
|
|
92
|
+
const url = new URL(value);
|
|
93
|
+
return url.protocol === 'https:' && !url.username && !url.password ? url.toString() : undefined;
|
|
94
|
+
} catch (unused) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** The fields of a stored media document the picker hands on, from the document. */ export function stockPhotoPickerMediaOf(mediaId, data) {
|
|
99
|
+
const text = (key)=>typeof data[key] === 'string' ? data[key] : undefined;
|
|
100
|
+
const count = (key)=>typeof data[key] === 'number' && Number.isFinite(data[key]) ? data[key] : undefined;
|
|
101
|
+
const list = (key, keep)=>Array.isArray(data[key]) ? data[key].filter(keep) : undefined;
|
|
102
|
+
const media = {
|
|
103
|
+
$id: mediaId
|
|
104
|
+
};
|
|
105
|
+
const fields = {
|
|
106
|
+
fileName: text('fileName'),
|
|
107
|
+
contentType: text('contentType'),
|
|
108
|
+
sizeBytes: count('sizeBytes'),
|
|
109
|
+
url: text('url'),
|
|
110
|
+
cdnPath: text('cdnPath'),
|
|
111
|
+
alt: text('alt'),
|
|
112
|
+
description: text('description'),
|
|
113
|
+
width: count('width'),
|
|
114
|
+
height: count('height'),
|
|
115
|
+
variants: list('variants', (item)=>typeof item === 'number'),
|
|
116
|
+
visibleTo: list('visibleTo', (item)=>typeof item === 'string')
|
|
117
|
+
};
|
|
118
|
+
for (const [key, value] of Object.entries(fields)){
|
|
119
|
+
if (value !== undefined) media[key] = value;
|
|
120
|
+
}
|
|
121
|
+
if (typeof data['folderId'] === 'string' || data['folderId'] === null) media.folderId = data['folderId'];
|
|
122
|
+
return media;
|
|
123
|
+
}
|
|
124
|
+
/** Said when a request failed before the server could answer it. */ const UNREACHABLE = 'Stock photos could not be reached. Check your connection and try again.';
|
|
125
|
+
/** What a refusal says: the route's own words, else `fallback`. */ async function refusalOf(response, fallback) {
|
|
126
|
+
const body = await response.json().catch(()=>null);
|
|
127
|
+
const said = typeof (body == null ? void 0 : body.message) === 'string' ? body.message : typeof (body == null ? void 0 : body.error) === 'string' ? body.error : '';
|
|
128
|
+
return {
|
|
129
|
+
status: 'refused',
|
|
130
|
+
message: said || fallback
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** The JSON answer of a request, or a refusal that says why there is none. */ async function answerOf(request) {
|
|
134
|
+
let response;
|
|
135
|
+
try {
|
|
136
|
+
response = await request();
|
|
137
|
+
} catch (unused) {
|
|
138
|
+
return {
|
|
139
|
+
status: 'refused',
|
|
140
|
+
message: UNREACHABLE
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!response.ok) return refusalOf(response, UNREACHABLE);
|
|
144
|
+
const body = await response.json().catch(()=>null);
|
|
145
|
+
return body && typeof body.status === 'string' ? body : {
|
|
146
|
+
status: 'refused',
|
|
147
|
+
message: UNREACHABLE
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The client for one library. `post` carries the member's credentials: the
|
|
152
|
+
* console hands in `authorizedFetch` bound to the route.
|
|
153
|
+
*/ export function createStockPhotoPickerClient(post, target) {
|
|
154
|
+
const library = target.hostId ? {
|
|
155
|
+
hostId: target.hostId
|
|
156
|
+
} : {
|
|
157
|
+
orgId: target.orgId
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
status: ()=>answerOf(()=>post(_extends({
|
|
161
|
+
action: 'status'
|
|
162
|
+
}, library))),
|
|
163
|
+
search: (query, orientation)=>answerOf(()=>post(_extends({
|
|
164
|
+
action: 'search'
|
|
165
|
+
}, library, {
|
|
166
|
+
query,
|
|
167
|
+
orientation
|
|
168
|
+
}))),
|
|
169
|
+
async preview (ref) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await post(_extends({
|
|
172
|
+
action: 'preview'
|
|
173
|
+
}, library, {
|
|
174
|
+
ref
|
|
175
|
+
}));
|
|
176
|
+
if (response.status !== 200) return null;
|
|
177
|
+
const blob = await response.blob();
|
|
178
|
+
return blob.size && blob.type.startsWith('image/') ? blob : null;
|
|
179
|
+
} catch (unused) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
importPhoto: (ref, options)=>answerOf(()=>post(_extends({
|
|
184
|
+
action: 'import'
|
|
185
|
+
}, library, {
|
|
186
|
+
ref
|
|
187
|
+
}, (options == null ? void 0 : options.folderId) ? {
|
|
188
|
+
folderId: options.folderId
|
|
189
|
+
} : {})))
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=stock-photo-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/aglyn/src/lib/app-utils/stock-photos/stock-photo-picker.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 STOCK_PHOTO_QUERY_MAX_CHARS,\n type StockPhotoOrientation,\n} from '../../plugin-manager/stock-photo-provider'\n\n/**\n * STOCK PHOTOS IN THE MEDIA PICKER (AGL-4066): the wire between the\n * console's media picker and its server route, `/api/media/stock-photos`.\n *\n * A person editing a site by hand searches the deployment's stock photo\n * library from the picker's Stock photos tab and places one. The route asks\n * core's `core.stock-photos` provider and copies the chosen photo into the\n * library the picker is filing into through `core.media-ingest`, as the\n * signed-in member; the placed image is always that copy, never an address\n * at the library. This module is the shapes both sides agree on, the checks\n * the server makes on what a browser sends, and the browser's client. It\n * names no library: the provider's own label comes back in every answer.\n *\n * Every request names one library, the target: a site's (`hostId`) or the\n * workspace's shared one (`orgId`). A hit is named back to the server by\n * {@link StockPhotoHitRef} — the search that found it and its id — never by\n * the addresses or the credit, so the server finds the photo again in the\n * provider's own cached answer and nothing a browser claims about a photo\n * is stored.\n */\n\n/** The console route the picker's Stock photos tab talks to. */\nexport const STOCK_PHOTO_PICKER_ROUTE = '/api/media/stock-photos'\n\n/** The library a request searches for and copies into. */\nexport type StockPhotoPickerTarget = { hostId: string; orgId?: undefined } | { orgId: string; hostId?: undefined }\n\n/** The orientations the tab offers, in its order. */\nexport const STOCK_PHOTO_PICKER_ORIENTATIONS: readonly StockPhotoOrientation[] = ['any', 'horizontal', 'vertical']\n\n/** How a search result names its photo back to the server. */\nexport interface StockPhotoHitRef {\n /** The search words exactly as searched, after {@link normalizeStockPhotoQuery}. */\n query: string\n orientation: StockPhotoOrientation\n /** The library's own id for the photo. */\n id: string\n}\n\n/** One photo a search found, as the tab shows it. */\nexport interface StockPhotoPickerHit {\n ref: StockPhotoHitRef\n /** `{provider}:{id}`: what the library reuses an earlier copy by. */\n sourceKey: string\n /** The size of the copy that is stored when the photo is used. */\n width: number\n height: number\n photographer: string\n photographerUrl?: string\n /** The photo's page at the library. */\n pageUrl: string\n /** The library's tags, a few, lowercase. */\n tags: string[]\n /** The library's own description of the photo, where it writes one. */\n alt?: string\n /** The sentence the stored copy's description carries: \"Photo by … on …\". */\n credit: string\n license: string\n licenseUrl: string\n attributionRequired: boolean\n}\n\n/** The library the deployment searches, by the name its terms ask for. */\nexport interface StockPhotoPickerLibrary {\n label: string\n}\n\n/** What the tab is told when it opens. */\nexport type StockPhotoStatusAnswer =\n | { status: 'ready'; library: StockPhotoPickerLibrary }\n | { status: 'unconfigured'; message: string }\n | { status: 'refused'; message: string }\n\n/** What a search answers. `busy`: the library could not be asked just now. */\nexport type StockPhotoSearchAnswer =\n | { status: 'ok'; library: StockPhotoPickerLibrary; query: string; orientation: StockPhotoOrientation; hits: StockPhotoPickerHit[] }\n | { status: 'busy'; message: string }\n | { status: 'unconfigured'; message: string }\n | { status: 'refused'; message: string }\n\n/**\n * A stored asset as the picker places it: the media document's own fields,\n * the ones a library pick hands on.\n */\nexport interface StockPhotoPickerMedia {\n $id: string\n fileName?: string\n contentType?: string\n sizeBytes?: number\n url?: string\n cdnPath?: string\n folderId?: string | null\n alt?: string\n description?: string\n width?: number\n height?: number\n variants?: number[]\n visibleTo?: string[]\n}\n\n/** What using a photo answers. `reused`: the library already held a copy, which is placed. */\nexport type StockPhotoImportAnswer =\n | { status: 'ok'; reused: boolean; media: StockPhotoPickerMedia }\n | { status: 'busy'; message: string }\n | { status: 'gone'; message: string }\n | { status: 'unconfigured'; message: string }\n | { status: 'refused'; message: string }\n\n/** Said when the deployment has no stock photo library. */\nexport const STOCK_PHOTOS_UNCONFIGURED_MESSAGE =\n 'Stock photos are not set up here, so there is no library to search. ' +\n 'You can still upload your own photos in the library tabs.'\n\n/** Said when the library could not be asked, or could not send a photo, just now. */\nexport function stockPhotosBusyMessage(label: string): string {\n return `${label || 'The stock photo library'} is busy right now. Try again in a minute.`\n}\n\n/** Said when the photo is no longer among the search's results. */\nexport const STOCK_PHOTO_GONE_MESSAGE =\n 'That photo is no longer in the search results. Search again and choose it from the new results.'\n\n/** The longest id a library may give a photo here. */\nconst MAX_ID_CHARS = 64\n\n/** A library id: a site's or a workspace's document id, never a path. */\nconst isDocumentId = (value: unknown): value is string =>\n typeof value === 'string' && value.length > 0 && value.length <= 128 && !value.includes('/') && value.trim() === value\n\n/**\n * The search words as searched: control characters dropped, runs of space\n * made one, trimmed. `null` when nothing is left or it is longer than\n * {@link STOCK_PHOTO_QUERY_MAX_CHARS}.\n */\nexport function normalizeStockPhotoQuery(value: unknown): string | null {\n if (typeof value !== 'string') return null\n // eslint-disable-next-line no-control-regex\n const query = value.replace(/[\\u0000-\\u001f\\u007f]/g, ' ').replace(/\\s+/g, ' ').trim()\n if (!query || query.length > STOCK_PHOTO_QUERY_MAX_CHARS) return null\n return query\n}\n\n/** An orientation the tab offers, `any` for anything else. */\nexport function stockPhotoOrientationOf(value: unknown): StockPhotoOrientation {\n return STOCK_PHOTO_PICKER_ORIENTATIONS.includes(value as StockPhotoOrientation)\n ? (value as StockPhotoOrientation)\n : 'any'\n}\n\n/** The library a request names, or `null` when it names none, or both. */\nexport function stockPhotoTargetOf(body: unknown): StockPhotoPickerTarget | null {\n const record = (body ?? {}) as Record<string, unknown>\n const hostId = record['hostId']\n const orgId = record['orgId']\n const hasHost = hostId !== undefined && hostId !== null && hostId !== ''\n const hasOrg = orgId !== undefined && orgId !== null && orgId !== ''\n if (hasHost === hasOrg) return null\n if (hasHost) return isDocumentId(hostId) ? { hostId } : null\n return isDocumentId(orgId) ? { orgId } : null\n}\n\n/** The photo a request names, or `null` when it is not one a search could have found. */\nexport function stockPhotoHitRefOf(value: unknown): StockPhotoHitRef | null {\n const record = (value ?? {}) as Record<string, unknown>\n const query = normalizeStockPhotoQuery(record['query'])\n const id = record['id']\n if (!query || typeof id !== 'string' || !id || id.length > MAX_ID_CHARS || !/^[\\w.-]+$/.test(id)) return null\n return { query, orientation: stockPhotoOrientationOf(record['orientation']), id }\n}\n\n/** `value` when it is an https address, else `undefined`: nothing else is ever linked. */\nexport function stockPhotoSafeLink(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n try {\n const url = new URL(value)\n return url.protocol === 'https:' && !url.username && !url.password ? url.toString() : undefined\n } catch {\n return undefined\n }\n}\n\n/** The fields of a stored media document the picker hands on, from the document. */\nexport function stockPhotoPickerMediaOf(mediaId: string, data: Record<string, unknown>): StockPhotoPickerMedia {\n const text = (key: string) => (typeof data[key] === 'string' ? (data[key] as string) : undefined)\n const count = (key: string) => (typeof data[key] === 'number' && Number.isFinite(data[key]) ? (data[key] as number) : undefined)\n const list = <T>(key: string, keep: (item: unknown) => item is T): T[] | undefined =>\n Array.isArray(data[key]) ? (data[key] as unknown[]).filter(keep) : undefined\n const media: StockPhotoPickerMedia = { $id: mediaId }\n const fields: Partial<StockPhotoPickerMedia> = {\n fileName: text('fileName'),\n contentType: text('contentType'),\n sizeBytes: count('sizeBytes'),\n url: text('url'),\n cdnPath: text('cdnPath'),\n alt: text('alt'),\n description: text('description'),\n width: count('width'),\n height: count('height'),\n variants: list('variants', (item): item is number => typeof item === 'number'),\n visibleTo: list('visibleTo', (item): item is string => typeof item === 'string'),\n }\n for (const [key, value] of Object.entries(fields)) {\n if (value !== undefined) (media as unknown as Record<string, unknown>)[key] = value\n }\n if (typeof data['folderId'] === 'string' || data['folderId'] === null) media.folderId = data['folderId'] as string | null\n return media\n}\n\n/** Posts one JSON request to the route, with whatever credentials the caller adds. */\nexport type StockPhotoPickerPost = (body: Record<string, unknown>) => Promise<Response>\n\n/** The browser's side of the route, for one library. */\nexport interface StockPhotoPickerClient {\n status(): Promise<StockPhotoStatusAnswer>\n search(query: string, orientation: StockPhotoOrientation): Promise<StockPhotoSearchAnswer>\n /** The hit's small copy, or `null` when none can be had. */\n preview(ref: StockPhotoHitRef): Promise<Blob | null>\n /** Copies the photo into the library, or finds the copy it holds. */\n importPhoto(ref: StockPhotoHitRef, options?: { folderId?: string | null }): Promise<StockPhotoImportAnswer>\n}\n\n/** Said when a request failed before the server could answer it. */\nconst UNREACHABLE = 'Stock photos could not be reached. Check your connection and try again.'\n\n/** What a refusal says: the route's own words, else `fallback`. */\nasync function refusalOf(response: Response, fallback: string): Promise<{ status: 'refused'; message: string }> {\n const body = (await response.json().catch((): null => null)) as { message?: unknown; error?: unknown } | null\n const said = typeof body?.message === 'string' ? body.message : typeof body?.error === 'string' ? body.error : ''\n return { status: 'refused', message: said || fallback }\n}\n\n/** The JSON answer of a request, or a refusal that says why there is none. */\nasync function answerOf<Answer>(request: () => Promise<Response>): Promise<Answer | { status: 'refused'; message: string }> {\n let response: Response\n try {\n response = await request()\n } catch {\n return { status: 'refused', message: UNREACHABLE }\n }\n if (!response.ok) return refusalOf(response, UNREACHABLE)\n const body = (await response.json().catch((): null => null)) as Answer | null\n return body && typeof (body as { status?: unknown }).status === 'string' ? body : { status: 'refused', message: UNREACHABLE }\n}\n\n/**\n * The client for one library. `post` carries the member's credentials: the\n * console hands in `authorizedFetch` bound to the route.\n */\nexport function createStockPhotoPickerClient(\n post: StockPhotoPickerPost,\n target: StockPhotoPickerTarget,\n): StockPhotoPickerClient {\n const library = target.hostId ? { hostId: target.hostId } : { orgId: target.orgId }\n return {\n status: () => answerOf<StockPhotoStatusAnswer>(() => post({ action: 'status', ...library })),\n search: (query, orientation) =>\n answerOf<StockPhotoSearchAnswer>(() => post({ action: 'search', ...library, query, orientation })),\n async preview(ref) {\n try {\n const response = await post({ action: 'preview', ...library, ref })\n if (response.status !== 200) return null\n const blob = await response.blob()\n return blob.size && blob.type.startsWith('image/') ? blob : null\n } catch {\n return null\n }\n },\n importPhoto: (ref, options) =>\n answerOf<StockPhotoImportAnswer>(() =>\n post({\n action: 'import',\n ...library,\n ref,\n ...(options?.folderId ? { folderId: options.folderId } : {}),\n }),\n ),\n }\n}\n"],"names":["STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PICKER_ROUTE","STOCK_PHOTO_PICKER_ORIENTATIONS","STOCK_PHOTOS_UNCONFIGURED_MESSAGE","stockPhotosBusyMessage","label","STOCK_PHOTO_GONE_MESSAGE","MAX_ID_CHARS","isDocumentId","value","length","includes","trim","normalizeStockPhotoQuery","query","replace","stockPhotoOrientationOf","stockPhotoTargetOf","body","record","hostId","orgId","hasHost","undefined","hasOrg","stockPhotoHitRefOf","id","test","orientation","stockPhotoSafeLink","url","URL","protocol","username","password","toString","stockPhotoPickerMediaOf","mediaId","data","text","key","count","Number","isFinite","list","keep","Array","isArray","filter","media","$id","fields","fileName","contentType","sizeBytes","cdnPath","alt","description","width","height","variants","item","visibleTo","Object","entries","folderId","UNREACHABLE","refusalOf","response","fallback","json","catch","said","message","error","status","answerOf","request","ok","createStockPhotoPickerClient","post","target","library","action","search","preview","ref","blob","size","type","startsWith","importPhoto","options"],"mappings":"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"}
|
|
@@ -35,7 +35,18 @@ import type { AglynHostMediaImportSource, AglynHostMediaStockSource } from '../f
|
|
|
35
35
|
* {@link pluginMediaIngest}, and a caller keeps its own fallback.
|
|
36
36
|
*/
|
|
37
37
|
export interface PluginMediaIngestRequest {
|
|
38
|
+
/**
|
|
39
|
+
* The site whose library the asset is stored in; with {@link orgId}, the
|
|
40
|
+
* site on screen, if any, which the workspace's Default sharing may narrow
|
|
41
|
+
* the asset to, and otherwise empty.
|
|
42
|
+
*/
|
|
38
43
|
hostId: string;
|
|
44
|
+
/**
|
|
45
|
+
* The workspace whose SHARED library the asset is stored in instead of a
|
|
46
|
+
* site's (AGL-4066): the media picker opened with no site, adding a stock
|
|
47
|
+
* photo. Absent, the site's own library.
|
|
48
|
+
*/
|
|
49
|
+
orgId?: string;
|
|
39
50
|
/**
|
|
40
51
|
* The member the asset is stored as. Their access to the site, the site's
|
|
41
52
|
* lockdown and the workspace's plan and storage band are checked as for
|
|
@@ -92,10 +103,14 @@ export interface PluginMediaIngest {
|
|
|
92
103
|
/**
|
|
93
104
|
* The site's live, public asset copied from a stock photo with this source
|
|
94
105
|
* key (`{provider}:{id}`), so a photo is never stored twice in one library.
|
|
106
|
+
* With `orgId`, the workspace library's asset (AGL-4066), taken only where
|
|
107
|
+
* it is shared with every site, or with `hostId`'s when one is named, so a
|
|
108
|
+
* member is never handed a file their sharing keeps from them.
|
|
95
109
|
*/
|
|
96
110
|
findStockPhoto(input: {
|
|
97
111
|
hostId: string;
|
|
98
112
|
sourceKey: string;
|
|
113
|
+
orgId?: string;
|
|
99
114
|
}): Promise<PluginMediaAsset | null>;
|
|
100
115
|
/**
|
|
101
116
|
* The site's live, public photos of the business itself (AGL-3957): what
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.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 {\n AglynHostMediaImportSource,\n AglynHostMediaStockSource,\n} from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n /**\n * The library folder the asset is filed in (AGL-3957); absent or null,\n * the library's root. An id the library does not hold files the asset at\n * the root, as an upload naming a stale folder does.\n */\n folderId?: string | null\n /**\n * The asset is the business's own photo, sent through a site photo link\n * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on\n * the asset, which is what the AI reads its owner's pictures by.\n */\n ownerPhotoLinkId?: string\n /**\n * The website a site import brought the picture in from (AGL-3976),\n * stamped as `importedFrom` on the asset; {@link PluginMediaIngest.findImportedPhoto}\n * reads it back by its key, so one picture is stored once per site.\n */\n importedFrom?: Omit<AglynHostMediaImportSource, 'importedAt'>\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n /** What the asset's own alt text says it shows, where a member wrote one. */\n alt?: string\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: {
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.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 {\n AglynHostMediaImportSource,\n AglynHostMediaStockSource,\n} from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n /**\n * The site whose library the asset is stored in; with {@link orgId}, the\n * site on screen, if any, which the workspace's Default sharing may narrow\n * the asset to, and otherwise empty.\n */\n hostId: string\n /**\n * The workspace whose SHARED library the asset is stored in instead of a\n * site's (AGL-4066): the media picker opened with no site, adding a stock\n * photo. Absent, the site's own library.\n */\n orgId?: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n /**\n * The library folder the asset is filed in (AGL-3957); absent or null,\n * the library's root. An id the library does not hold files the asset at\n * the root, as an upload naming a stale folder does.\n */\n folderId?: string | null\n /**\n * The asset is the business's own photo, sent through a site photo link\n * (AGL-3957) — the id of the link it came through. Stamps `ownerPhoto` on\n * the asset, which is what the AI reads its owner's pictures by.\n */\n ownerPhotoLinkId?: string\n /**\n * The website a site import brought the picture in from (AGL-3976),\n * stamped as `importedFrom` on the asset; {@link PluginMediaIngest.findImportedPhoto}\n * reads it back by its key, so one picture is stored once per site.\n */\n importedFrom?: Omit<AglynHostMediaImportSource, 'importedAt'>\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n /** What the asset's own alt text says it shows, where a member wrote one. */\n alt?: string\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n * With `orgId`, the workspace library's asset (AGL-4066), taken only where\n * it is shared with every site, or with `hostId`'s when one is named, so a\n * member is never handed a file their sharing keeps from them.\n */\n findStockPhoto(input: {\n hostId: string\n sourceKey: string\n orgId?: string\n }): Promise<PluginMediaAsset | null>\n /**\n * The site's live, public photos of the business itself (AGL-3957): what\n * came through a site photo link, newest first, at most `limit`. A photo\n * of the owner's own work is placed before any stock photo is searched\n * for. Optional: an implementation registered before the link existed\n * answers nothing, and a caller treats that as an empty library.\n */\n listOwnerPhotos?(input: { hostId: string; limit?: number }): Promise<PluginMediaAsset[]>\n /**\n * The site's live, public asset a site import already brought in under this\n * source key (`import:<hash>`, AGL-3976), so the same picture is never\n * stored twice. Optional: an implementation registered before imports\n * existed answers nothing, and an import then stores no picture at all\n * rather than risk a second copy.\n */\n findImportedPhoto?(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AAqH1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
|
|
@@ -120,6 +120,18 @@ export interface StockPhotoProvider {
|
|
|
120
120
|
}): Promise<StockPhotoDownload | null>;
|
|
121
121
|
/** How an asset copied from `photo` credits it. */
|
|
122
122
|
credit(photo: StockPhoto): StockPhotoCredit;
|
|
123
|
+
/**
|
|
124
|
+
* A small copy of the photo for showing it as a search result (AGL-4066),
|
|
125
|
+
* fetched only from the library's own image hosts, or `null` when none can
|
|
126
|
+
* be had within `maxBytes` and the signal. A server process asks for it and
|
|
127
|
+
* hands the bytes to the member choosing, so a browser never loads an
|
|
128
|
+
* address at the library. Optional: a provider without it shows no
|
|
129
|
+
* thumbnails, and a placed photo is always the copy {@link download} fetched.
|
|
130
|
+
*/
|
|
131
|
+
preview?(photo: StockPhoto, options: {
|
|
132
|
+
maxBytes: number;
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
}): Promise<StockPhotoDownload | null>;
|
|
123
135
|
}
|
|
124
136
|
/**
|
|
125
137
|
* Many plugins may each register a library; a caller is handed the first
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.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 definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n /**\n * What the photo shows, in words a page's `alt` takes as they stand\n * (AGL-3856): the photo's own where the subject's words do not describe it.\n */\n alt?: string\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA+E9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAqEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.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 definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n /**\n * A small copy of the photo for showing it as a search result (AGL-4066),\n * fetched only from the library's own image hosts, or `null` when none can\n * be had within `maxBytes` and the signal. A server process asks for it and\n * hands the bytes to the member choosing, so a browser never loads an\n * address at the library. Optional: a provider without it shows no\n * thumbnails, and a placed photo is always the copy {@link download} fetched.\n */\n preview?(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n /**\n * What the photo shows, in words a page's `alt` takes as they stand\n * (AGL-3856): the photo's own where the subject's words do not describe it.\n */\n alt?: string\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA2F9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAqEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|