@prenta/core 7.7.1 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/api/handler-factory.d.ts.map +1 -1
  3. package/dist/api/handler-factory.js +40 -14
  4. package/dist/api/handler-factory.js.map +1 -1
  5. package/dist/api/openapi.d.ts.map +1 -1
  6. package/dist/api/openapi.js +8 -2
  7. package/dist/api/openapi.js.map +1 -1
  8. package/dist/api/route-helpers/auth.d.ts +131 -0
  9. package/dist/api/route-helpers/auth.d.ts.map +1 -0
  10. package/dist/api/route-helpers/auth.js +505 -0
  11. package/dist/api/route-helpers/auth.js.map +1 -0
  12. package/dist/api/route-helpers/authoring.d.ts +32 -0
  13. package/dist/api/route-helpers/authoring.d.ts.map +1 -0
  14. package/dist/api/route-helpers/authoring.js +99 -0
  15. package/dist/api/route-helpers/authoring.js.map +1 -0
  16. package/dist/api/route-helpers/forms.d.ts +46 -0
  17. package/dist/api/route-helpers/forms.d.ts.map +1 -0
  18. package/dist/api/route-helpers/forms.js +435 -0
  19. package/dist/api/route-helpers/forms.js.map +1 -0
  20. package/dist/api/route-helpers/http.d.ts +87 -0
  21. package/dist/api/route-helpers/http.d.ts.map +1 -0
  22. package/dist/api/route-helpers/http.js +286 -0
  23. package/dist/api/route-helpers/http.js.map +1 -0
  24. package/dist/api/route-helpers/media.d.ts +37 -0
  25. package/dist/api/route-helpers/media.d.ts.map +1 -0
  26. package/dist/api/route-helpers/media.js +189 -0
  27. package/dist/api/route-helpers/media.js.map +1 -0
  28. package/dist/api/route-helpers/page-shape.d.ts +31 -0
  29. package/dist/api/route-helpers/page-shape.d.ts.map +1 -0
  30. package/dist/api/route-helpers/page-shape.js +98 -0
  31. package/dist/api/route-helpers/page-shape.js.map +1 -0
  32. package/dist/api/route-helpers.d.ts +8 -358
  33. package/dist/api/route-helpers.d.ts.map +1 -1
  34. package/dist/api/route-helpers.js +7 -1592
  35. package/dist/api/route-helpers.js.map +1 -1
  36. package/dist/api/routes/users/handlers/api-keys.d.ts +4 -0
  37. package/dist/api/routes/users/handlers/api-keys.d.ts.map +1 -0
  38. package/dist/api/routes/users/handlers/api-keys.js +164 -0
  39. package/dist/api/routes/users/handlers/api-keys.js.map +1 -0
  40. package/dist/api/routes/users/handlers/invites.d.ts +7 -0
  41. package/dist/api/routes/users/handlers/invites.d.ts.map +1 -0
  42. package/dist/api/routes/users/handlers/invites.js +201 -0
  43. package/dist/api/routes/users/handlers/invites.js.map +1 -0
  44. package/dist/api/routes/users/handlers/users-admin.d.ts +7 -0
  45. package/dist/api/routes/users/handlers/users-admin.d.ts.map +1 -0
  46. package/dist/api/routes/users/handlers/users-admin.js +310 -0
  47. package/dist/api/routes/users/handlers/users-admin.js.map +1 -0
  48. package/dist/api/routes/users/handlers/users-detail.d.ts +16 -0
  49. package/dist/api/routes/users/handlers/users-detail.d.ts.map +1 -0
  50. package/dist/api/routes/users/handlers/users-detail.js +525 -0
  51. package/dist/api/routes/users/handlers/users-detail.js.map +1 -0
  52. package/dist/api/routes/users.d.ts +4 -0
  53. package/dist/api/routes/users.d.ts.map +1 -1
  54. package/dist/api/routes/users.js +38 -1177
  55. package/dist/api/routes/users.js.map +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,286 @@
1
+ import { publicReadCacheControl } from '../../cache/http.js';
2
+ import { createLogger } from '../../diagnostics/logger.js';
3
+ import { captureServerException } from '../../diagnostics/sentry.js';
4
+ import { getPrentaConfig } from '../../config/runtime.js';
5
+ import { ModelNotAvailableError } from '../guarded-db.js';
6
+ import { SECURITY_HEADERS } from '../security-headers.js';
7
+ export const logger = createLogger('api');
8
+ export function json(data, status = 200) {
9
+ return new Response(JSON.stringify(data), {
10
+ status,
11
+ headers: { ...SECURITY_HEADERS },
12
+ });
13
+ }
14
+ /**
15
+ * Variant of `json()` that emits the Phase-5.5 public-read
16
+ * `Cache-Control` header when the active CMS config opts in.
17
+ *
18
+ * - Session-cookie requests always get `private, no-store`.
19
+ * - API-key / unauthenticated requests get
20
+ * `public, s-maxage=N, stale-while-revalidate=M` when
21
+ * `cache.publicReads.enabled` is true.
22
+ * - Otherwise no `Cache-Control` header is set (i.e. the response is
23
+ * uncacheable downstream, matching the pre-5.5 behaviour).
24
+ *
25
+ * Use this for the public read endpoints (`/collections/:slug`,
26
+ * `/collections/:slug/:id`, `/globals/:slug`, `/resolve`). Mutating
27
+ * endpoints stay on the plain `json()` so the CDN never caches a write
28
+ * response by accident.
29
+ */
30
+ export function jsonWithCache(data, request, status = 200) {
31
+ const cacheControl = publicReadCacheControl(request, getPrentaConfig()?.cache);
32
+ const headers = { ...SECURITY_HEADERS };
33
+ if (cacheControl)
34
+ headers['Cache-Control'] = cacheControl;
35
+ return new Response(JSON.stringify(data), { status, headers });
36
+ }
37
+ export function errorResponse(message, status) {
38
+ return json({ error: message }, status);
39
+ }
40
+ export function internalError(err, context) {
41
+ if (err instanceof ModelNotAvailableError) {
42
+ return modelNotAvailable(err.model);
43
+ }
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ logger.error(msg, context ? { context } : undefined);
46
+ // Fire-and-forget: capture lazily imports the optional @sentry/node and never
47
+ // throws, so we don't await it before returning the error response.
48
+ void captureServerException(err, { scope: 'api', extra: context ? { context } : undefined });
49
+ if (process.env.PRENTA_DEBUG_ERRORS === '1') {
50
+ return errorResponse(msg, 500);
51
+ }
52
+ return errorResponse('Internal server error', 500);
53
+ }
54
+ /**
55
+ * Map a thrown action-layer error to the right HTTP status. The data layer
56
+ * throws plain `Error`s with stable, human-readable prefixes for client-fixable
57
+ * problems (publish validation, access/permission, not-found); those are 4xx
58
+ * and the message is safe to return verbatim. Anything else falls through to
59
+ * {@link internalError} (500, generic message, logged).
60
+ */
61
+ export function actionError(err, context) {
62
+ if (err instanceof ModelNotAvailableError)
63
+ return modelNotAvailable(err.model);
64
+ const conflict = uniqueConstraintConflict(err);
65
+ if (conflict)
66
+ return errorResponse(conflict, 409);
67
+ const msg = err instanceof Error ? err.message : String(err);
68
+ if (/^Cannot publish —/.test(msg))
69
+ return errorResponse(msg, 422);
70
+ if (/^Invalid media field/i.test(msg))
71
+ return errorResponse(msg, 400);
72
+ if (/insufficient permissions/i.test(msg) ||
73
+ /^Access denied/i.test(msg) ||
74
+ /submit for review before publishing/i.test(msg)) {
75
+ return errorResponse(msg, 403);
76
+ }
77
+ if (/not found/i.test(msg))
78
+ return errorResponse(msg, 404);
79
+ return internalError(err, context);
80
+ }
81
+ /**
82
+ * Detect a Prisma unique-constraint violation (P2002) via its stable `code`,
83
+ * without importing the heavy Prisma error class. Returns a client-safe 409
84
+ * message, or null when `err` is not a unique-constraint error. The usual
85
+ * trigger is a duplicate (collection, slug) on create/rename/publish — which
86
+ * previously fell through to a 500 (#560); a genuine live-slug clash is a
87
+ * client-fixable conflict, so it belongs at 409.
88
+ */
89
+ function uniqueConstraintConflict(err) {
90
+ if (!err || typeof err !== 'object' || err.code !== 'P2002') {
91
+ return null;
92
+ }
93
+ const target = err.meta?.target;
94
+ const fields = Array.isArray(target)
95
+ ? target.map(String)
96
+ : typeof target === 'string'
97
+ ? [target]
98
+ : [];
99
+ if (fields.some((f) => f.toLowerCase().includes('slug'))) {
100
+ return 'A document with this slug already exists in this collection.';
101
+ }
102
+ return 'That value is already in use.';
103
+ }
104
+ export function clampPageSize(raw, max = 100, fallback = 20) {
105
+ const n = Number(raw) || fallback;
106
+ return Math.min(Math.max(1, n), max);
107
+ }
108
+ /**
109
+ * Clamp a 1-based list `page` query param. Negative / zero / NaN values must
110
+ * never reach Prisma `skip` (negative OFFSET → validation error → opaque 500).
111
+ * Mirrors forms/users/redirects (`Math.max(1, …)`).
112
+ */
113
+ export function clampPage(raw, fallback = 1) {
114
+ const n = Number(raw);
115
+ if (!Number.isFinite(n) || n < 1)
116
+ return fallback;
117
+ return Math.min(Math.floor(n), 100_000);
118
+ }
119
+ /** Parse a JSON object body; invalid JSON or a non-object yields a 400. */
120
+ export async function readJsonBody(request) {
121
+ try {
122
+ const parsed = await request.json();
123
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
124
+ return { ok: false, error: errorResponse('Invalid JSON body', 400) };
125
+ }
126
+ return { ok: true, data: parsed };
127
+ }
128
+ catch {
129
+ return { ok: false, error: errorResponse('Invalid JSON body', 400) };
130
+ }
131
+ }
132
+ export function asRecord(value) {
133
+ return value && typeof value === 'object' && !Array.isArray(value)
134
+ ? value
135
+ : {};
136
+ }
137
+ export function hasModel(d, name) {
138
+ try {
139
+ return d[name] && typeof d[name].findMany === 'function';
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ }
145
+ /** Format a number compactly, e.g. 8400 -> "8.4K", 1_200_000 -> "1.2M". */
146
+ export function formatCompactNumber(n) {
147
+ if (!Number.isFinite(n))
148
+ return '0';
149
+ if (Math.abs(n) >= 1_000_000)
150
+ return `${(n / 1_000_000).toFixed(1)}M`;
151
+ if (Math.abs(n) >= 1_000)
152
+ return `${(n / 1_000).toFixed(1)}K`;
153
+ return String(Math.round(n));
154
+ }
155
+ /** Minimal CSV line parser supporting double-quoted fields with escaped quotes. */
156
+ export function parseCsvLine(line) {
157
+ const out = [];
158
+ let cur = '';
159
+ let inQuotes = false;
160
+ for (let i = 0; i < line.length; i++) {
161
+ const ch = line[i];
162
+ if (inQuotes) {
163
+ if (ch === '"') {
164
+ if (line[i + 1] === '"') {
165
+ cur += '"';
166
+ i++;
167
+ }
168
+ else {
169
+ inQuotes = false;
170
+ }
171
+ }
172
+ else {
173
+ cur += ch;
174
+ }
175
+ }
176
+ else if (ch === '"') {
177
+ inQuotes = true;
178
+ }
179
+ else if (ch === ',') {
180
+ out.push(cur);
181
+ cur = '';
182
+ }
183
+ else {
184
+ cur += ch;
185
+ }
186
+ }
187
+ out.push(cur);
188
+ return out;
189
+ }
190
+ export function modelNotAvailable(name) {
191
+ return errorResponse(`The "${name}" model is not available in your Prisma schema. ` +
192
+ 'Run `prenta db:init` for new schemas, or carefully update the existing Prenta block, create/apply a Prisma migration, then regenerate Prisma Client. ' +
193
+ 'See https://prentacms.dev/docs/database-setup for required models.', 501);
194
+ }
195
+ /**
196
+ * XML-escape a value for safe inclusion in sitemap content. Sitemaps reject
197
+ * unescaped ampersands and angle brackets; URLs frequently contain `&` query
198
+ * separators, so this is non-optional.
199
+ */
200
+ export function escapeXml(value) {
201
+ return value
202
+ .replace(/&/g, '&amp;')
203
+ .replace(/</g, '&lt;')
204
+ .replace(/>/g, '&gt;')
205
+ .replace(/"/g, '&quot;')
206
+ .replace(/'/g, '&apos;');
207
+ }
208
+ /**
209
+ * URLs per sitemap file. The sitemaps protocol caps a single file at 50,000
210
+ * URLs, but the child-sitemap route must load each row's full `data` JSON blob
211
+ * (for path/noindex/images) into memory to build the XML — at 45k rows that is
212
+ * hundreds of MB per request on an unauthenticated, crawler-hit endpoint. We
213
+ * cap well below the protocol limit to bound per-request memory; collections
214
+ * larger than this are split across multiple `?page=N` files referenced from
215
+ * the sitemap index, so no URLs are lost.
216
+ */
217
+ export const SITEMAP_PAGE_SIZE = 10000;
218
+ /**
219
+ * Log unexpected query failures from the `safe*` wrappers.
220
+ *
221
+ * The wrappers exist so an optional Prisma model (e.g. a delegate
222
+ * that older schemas don't define) doesn't blow up the route — they
223
+ * return the empty value (`0` / `[]`) when the model isn't available.
224
+ * That defensive contract historically swallowed *every* error,
225
+ * including Prisma's "Unknown argument" rejections that flag a real
226
+ * application bug (see the `relationLoadStrategy` regression
227
+ * discovered while landing Phase 5.5 #5). Logging the underlying
228
+ * error preserves the defensive return semantics while making the
229
+ * actual class of bug visible in Vercel + Sentry logs.
230
+ *
231
+ * Kept private to this module — it's a logging contract, not a
232
+ * public API.
233
+ */
234
+ export function logSafeQueryFailure(scope, err) {
235
+ // Defensive: never throw from a logger that wraps `safe*` queries
236
+ // — the whole point of those wrappers is to keep the request hot
237
+ // path alive.
238
+ try {
239
+ const message = err instanceof Error ? err.message : String(err);
240
+ logger.warn(`${scope} query failed`, { error: message });
241
+ }
242
+ catch {
243
+ /* swallow */
244
+ }
245
+ }
246
+ export async function safeCount(model, where) {
247
+ if (!model || typeof model !== 'object')
248
+ return 0;
249
+ let countFn;
250
+ try {
251
+ countFn = model.count;
252
+ }
253
+ catch {
254
+ return 0;
255
+ }
256
+ if (typeof countFn !== 'function')
257
+ return 0;
258
+ try {
259
+ return await countFn.call(model, where ? { where } : undefined);
260
+ }
261
+ catch (err) {
262
+ logSafeQueryFailure('safeCount', err);
263
+ return 0;
264
+ }
265
+ }
266
+ export async function safeFindMany(model, args) {
267
+ if (!model || typeof model !== 'object')
268
+ return [];
269
+ let findManyFn;
270
+ try {
271
+ findManyFn = model.findMany;
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ if (typeof findManyFn !== 'function')
277
+ return [];
278
+ try {
279
+ return await findManyFn.call(model, args);
280
+ }
281
+ catch (err) {
282
+ logSafeQueryFailure('safeFindMany', err);
283
+ return [];
284
+ }
285
+ }
286
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../src/api/route-helpers/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export declare function importBlobStorage(): Promise<any>;
2
+ export declare function mediaUrl(storageKey: unknown): string;
3
+ export declare function normalizeMediaItem(media: any): any;
4
+ /**
5
+ * Map the `MediaUsage` join rows (with their related document) into the
6
+ * `{ page, path }[]` shape the admin drawer renders. Soft-deleted documents
7
+ * are excluded so trashed pages don't keep an asset looking "in use".
8
+ */
9
+ export declare function buildMediaUsedOn(mediaUsages: unknown): {
10
+ page: string;
11
+ path: string;
12
+ }[];
13
+ /**
14
+ * Derive a concise, Title-Cased title from AI-generated alt text. The alt
15
+ * text is already a model output describing the image, so the title stays
16
+ * grounded in real content rather than the filename.
17
+ */
18
+ export declare function deriveMediaTitleFromAlt(alt: string): string;
19
+ /**
20
+ * String-level gate before fetching a media `storageKey`. Delegates to
21
+ * {@link validateWebhookUrl} (IP canonicalization for decimal/octal/hex,
22
+ * CGNAT, metadata, etc.) so this allowlist cannot drift from the SSRF policy
23
+ * used by `safeFetch`. Also rejects `*.local` (mDNS) which webhook validation
24
+ * does not cover.
25
+ */
26
+ export declare function isAllowedStorageUrl(url: string): boolean;
27
+ /**
28
+ * Fetch image bytes for multimodal (vision) analysis through {@link safeFetch}
29
+ * (SSRF-safe: re-validates the host, DNS-resolves to defeat rebinding, no
30
+ * redirects). Returns `null` on any failure (non-image, oversize, blocked,
31
+ * network) so the caller transparently falls back to a context-only heuristic.
32
+ */
33
+ export declare function fetchImageForVision(url: string, knownMimeType?: string): Promise<{
34
+ base64: string;
35
+ mimeType: string;
36
+ } | null>;
37
+ //# sourceMappingURL=media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../../src/api/route-helpers/media.ts"],"names":[],"mappings":"AAQA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,GAAG,CAAC,CAGtD;AAED,wBAAgB,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,MAAM,CAMpD;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,GAAG,OA8C5C;AAeD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,CAcvF;AAsBD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa3D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASxD;AAKD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAgBtD"}
@@ -0,0 +1,189 @@
1
+ import { formatBytes } from '../../media/optimize.js';
2
+ import { calculateMediaSeoScore } from '../../media/seo.js';
3
+ import { safeFetch } from '../../security/safe-fetch.js';
4
+ import { validateWebhookUrl } from '../../security/webhook.js';
5
+ import { getPrentaConfig } from '../../config/runtime.js';
6
+ // Opaque dynamic import so Turbopack/webpack won't statically analyze the specifier.
7
+ // Returns { put, del, ... } from @vercel/blob when available.
8
+ export async function importBlobStorage() {
9
+ const mod = '@vercel/' + 'blob';
10
+ return import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ mod);
11
+ }
12
+ export function mediaUrl(storageKey) {
13
+ const value = String(storageKey ?? '');
14
+ if (!value)
15
+ return '';
16
+ return value.startsWith('http://') || value.startsWith('https://') || value.startsWith('/')
17
+ ? value
18
+ : '';
19
+ }
20
+ export function normalizeMediaItem(media) {
21
+ const width = typeof media.width === 'number' ? media.width : null;
22
+ const height = typeof media.height === 'number' ? media.height : null;
23
+ const sizeBytes = Number(media.fileSize ?? media.sizeBytes ?? 0);
24
+ const isDecorative = media.isDecorative === true;
25
+ const usedOnTitles = Array.isArray(media.mediaUsages)
26
+ ? media.mediaUsages
27
+ .map((usage) => usage?.document?.title)
28
+ .filter((title) => typeof title === 'string' && title.trim().length > 0)
29
+ : [];
30
+ // Derived on every read (never persisted) so the score can't drift from the
31
+ // metadata — see `media/seo.ts` for the weight contract.
32
+ const seo = calculateMediaSeoScore({
33
+ fileName: media.filename ?? media.name ?? null,
34
+ mimeType: media.mimeType ?? media.type ?? null,
35
+ altText: media.altText ?? media.altTag ?? null,
36
+ title: media.title ?? null,
37
+ caption: media.caption ?? null,
38
+ description: media.description ?? null,
39
+ focusKeyword: media.focusKeyword ?? null,
40
+ sizeBytes,
41
+ width,
42
+ height,
43
+ isDecorative,
44
+ usedOnTitles,
45
+ });
46
+ return {
47
+ ...media,
48
+ name: media.filename ?? media.name ?? '',
49
+ type: media.mimeType ?? media.type ?? '',
50
+ size: formatBytes(sizeBytes),
51
+ sizeBytes,
52
+ date: media.createdAt ?? media.updatedAt ?? null,
53
+ url: mediaUrl(media.storageKey ?? media.url),
54
+ dimensions: width && height ? `${width}x${height}` : undefined,
55
+ format: typeof media.mimeType === 'string' ? media.mimeType.split('/')[1] : undefined,
56
+ altTag: media.altText ?? media.altTag ?? '',
57
+ isDecorative,
58
+ ...(media.aiAltText != null ? { aiAltSuggestion: media.aiAltText } : {}),
59
+ title: media.title ?? '',
60
+ seoScore: seo.score,
61
+ seoGrade: seo.grade,
62
+ seoIssues: seo.issues,
63
+ };
64
+ }
65
+ /**
66
+ * Build the admin editor route for a document that references a media asset.
67
+ * Pages own the `/pages/*` namespace; post-type collections use
68
+ * `/posts/:type/:id/edit`; everything else falls back to the generic
69
+ * collection editor. Kept in sync with the route table in `AdminRoot.tsx`.
70
+ */
71
+ function mediaUsageEditPath(collection, id) {
72
+ if (collection === 'pages')
73
+ return `/pages/${id}/edit`;
74
+ const def = getPrentaConfig()?.collections?.[collection];
75
+ if (def?.type === 'post')
76
+ return `/posts/${collection}/${id}/edit`;
77
+ return `/collections/${collection}/${id}`;
78
+ }
79
+ /**
80
+ * Map the `MediaUsage` join rows (with their related document) into the
81
+ * `{ page, path }[]` shape the admin drawer renders. Soft-deleted documents
82
+ * are excluded so trashed pages don't keep an asset looking "in use".
83
+ */
84
+ export function buildMediaUsedOn(mediaUsages) {
85
+ if (!Array.isArray(mediaUsages))
86
+ return [];
87
+ const seen = new Set();
88
+ const out = [];
89
+ for (const usage of mediaUsages) {
90
+ const doc = usage?.document;
91
+ if (!doc || doc.deletedAt || seen.has(doc.id))
92
+ continue;
93
+ seen.add(doc.id);
94
+ out.push({
95
+ page: doc.title || '(untitled)',
96
+ path: mediaUsageEditPath(doc.collection, doc.id),
97
+ });
98
+ }
99
+ return out;
100
+ }
101
+ const MEDIA_TITLE_STOPWORDS = new Set([
102
+ 'a',
103
+ 'an',
104
+ 'the',
105
+ 'of',
106
+ 'and',
107
+ 'or',
108
+ 'to',
109
+ 'in',
110
+ 'on',
111
+ 'for',
112
+ 'with',
113
+ 'at',
114
+ 'by',
115
+ 'from',
116
+ 'is',
117
+ 'are',
118
+ 'as',
119
+ ]);
120
+ /**
121
+ * Derive a concise, Title-Cased title from AI-generated alt text. The alt
122
+ * text is already a model output describing the image, so the title stays
123
+ * grounded in real content rather than the filename.
124
+ */
125
+ export function deriveMediaTitleFromAlt(alt) {
126
+ if (!alt)
127
+ return '';
128
+ const firstClause = (alt.split(/[.;:\n]/)[0] ?? '').trim();
129
+ const titled = firstClause
130
+ .split(/\s+/)
131
+ .slice(0, 9)
132
+ .map((word, i) => {
133
+ const lower = word.toLowerCase();
134
+ if (i > 0 && MEDIA_TITLE_STOPWORDS.has(lower))
135
+ return lower;
136
+ return word.charAt(0).toUpperCase() + word.slice(1);
137
+ })
138
+ .join(' ');
139
+ return titled.slice(0, 80).replace(/[\s,]+$/, '');
140
+ }
141
+ /**
142
+ * String-level gate before fetching a media `storageKey`. Delegates to
143
+ * {@link validateWebhookUrl} (IP canonicalization for decimal/octal/hex,
144
+ * CGNAT, metadata, etc.) so this allowlist cannot drift from the SSRF policy
145
+ * used by `safeFetch`. Also rejects `*.local` (mDNS) which webhook validation
146
+ * does not cover.
147
+ */
148
+ export function isAllowedStorageUrl(url) {
149
+ if (!validateWebhookUrl(url).valid)
150
+ return false;
151
+ try {
152
+ const h = new URL(url).hostname.toLowerCase();
153
+ if (h.endsWith('.local'))
154
+ return false;
155
+ return true;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ }
161
+ /** Max image size (bytes) we'll pull into memory for vision analysis. */
162
+ const MAX_VISION_IMAGE_BYTES = 8 * 1024 * 1024;
163
+ /**
164
+ * Fetch image bytes for multimodal (vision) analysis through {@link safeFetch}
165
+ * (SSRF-safe: re-validates the host, DNS-resolves to defeat rebinding, no
166
+ * redirects). Returns `null` on any failure (non-image, oversize, blocked,
167
+ * network) so the caller transparently falls back to a context-only heuristic.
168
+ */
169
+ export async function fetchImageForVision(url, knownMimeType) {
170
+ try {
171
+ const resp = await safeFetch(url, { method: 'GET', timeoutMs: 5000 });
172
+ if (!resp.ok)
173
+ return null;
174
+ const contentType = ((resp.headers.get('content-type') ?? knownMimeType ?? '').split(';')[0] ?? '').trim();
175
+ if (!contentType.startsWith('image/'))
176
+ return null;
177
+ const declaredLength = Number(resp.headers.get('content-length') ?? '0');
178
+ if (declaredLength > MAX_VISION_IMAGE_BYTES)
179
+ return null;
180
+ const buffer = Buffer.from(await resp.arrayBuffer());
181
+ if (buffer.byteLength === 0 || buffer.byteLength > MAX_VISION_IMAGE_BYTES)
182
+ return null;
183
+ return { base64: buffer.toString('base64'), mimeType: contentType };
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../../src/api/route-helpers/media.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ export declare function normalizeRedirect(redirect: any): any;
2
+ export declare function normalizeSeoPage(page: any): {
3
+ id: any;
4
+ collection: any;
5
+ updatedAt: any;
6
+ url: {};
7
+ title: any;
8
+ score: number;
9
+ issues: number;
10
+ metaTitle: {};
11
+ metaDescription: {};
12
+ canonical: {};
13
+ schemaType: {};
14
+ };
15
+ /**
16
+ * Renders a 1200x630 SVG suitable for og:image. We don't pull in Satori/resvg
17
+ * because most integrators don't need PNG — major crawlers (Facebook, Twitter,
18
+ * LinkedIn, Slack, Discord) handle image/svg+xml correctly. Sites that
19
+ * specifically need PNG can override with their own /og endpoint via
20
+ * @vercel/og and point `seo.defaultOgImage` at it.
21
+ */
22
+ export declare function renderOgSvg(opts: {
23
+ title: string;
24
+ description?: string;
25
+ siteName?: string | null;
26
+ theme: 'light' | 'dark';
27
+ bg: string;
28
+ fg: string;
29
+ muted: string;
30
+ }): string;
31
+ //# sourceMappingURL=page-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page-shape.d.ts","sourceRoot":"","sources":["../../../src/api/route-helpers/page-shape.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,GAAG,OAS9C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG;;;;;;;;;;;;EAyBzC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,MAAM,CAyDT"}