dineway 0.1.3
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/LICENSE +9 -0
- package/README.md +89 -0
- package/dist/adapters-BlzWJG82.d.mts +106 -0
- package/dist/apply-CAPvMfoU.mjs +1339 -0
- package/dist/astro/index.d.mts +50 -0
- package/dist/astro/index.mjs +1326 -0
- package/dist/astro/middleware/auth.d.mts +30 -0
- package/dist/astro/middleware/auth.mjs +708 -0
- package/dist/astro/middleware/redirect.d.mts +21 -0
- package/dist/astro/middleware/redirect.mjs +62 -0
- package/dist/astro/middleware/request-context.d.mts +17 -0
- package/dist/astro/middleware/request-context.mjs +1371 -0
- package/dist/astro/middleware/setup.d.mts +19 -0
- package/dist/astro/middleware/setup.mjs +46 -0
- package/dist/astro/middleware.d.mts +12 -0
- package/dist/astro/middleware.mjs +1716 -0
- package/dist/astro/types.d.mts +269 -0
- package/dist/astro/types.mjs +1 -0
- package/dist/base64-F8-DUraK.mjs +58 -0
- package/dist/byline-DeWCMU_i.mjs +234 -0
- package/dist/bylines-DyqBV9EQ.mjs +137 -0
- package/dist/chunk-ClPoSABd.mjs +21 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +3987 -0
- package/dist/client/external-auth-headers.d.mts +38 -0
- package/dist/client/external-auth-headers.mjs +101 -0
- package/dist/client/index.d.mts +397 -0
- package/dist/client/index.mjs +345 -0
- package/dist/config-Cq8H0SfX.mjs +46 -0
- package/dist/connection-C9pxzuag.mjs +52 -0
- package/dist/content-zSgdNmnt.mjs +836 -0
- package/dist/db/index.d.mts +4 -0
- package/dist/db/index.mjs +62 -0
- package/dist/db/libsql.d.mts +10 -0
- package/dist/db/libsql.mjs +21 -0
- package/dist/db/postgres.d.mts +10 -0
- package/dist/db/postgres.mjs +29 -0
- package/dist/db/sqlite.d.mts +10 -0
- package/dist/db/sqlite.mjs +15 -0
- package/dist/default-WYlzADZL.mjs +80 -0
- package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
- package/dist/error-DrxtnGPg.mjs +26 -0
- package/dist/index-C-jx21qs.d.mts +4771 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +30 -0
- package/dist/load-C6FCD1FU.mjs +27 -0
- package/dist/loader-qKmo0wAY.mjs +446 -0
- package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
- package/dist/media/index.d.mts +25 -0
- package/dist/media/index.mjs +54 -0
- package/dist/media/local-runtime.d.mts +38 -0
- package/dist/media/local-runtime.mjs +132 -0
- package/dist/media-DMTr80Gv.mjs +199 -0
- package/dist/mode-BlyYtIFO.mjs +22 -0
- package/dist/page/index.d.mts +148 -0
- package/dist/page/index.mjs +419 -0
- package/dist/placeholder-B3knXwNc.mjs +267 -0
- package/dist/placeholder-bOx1xCTY.d.mts +283 -0
- package/dist/plugin-utils.d.mts +57 -0
- package/dist/plugin-utils.mjs +77 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
- package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
- package/dist/query-BiaPl_g2.mjs +459 -0
- package/dist/redirect-JPqLAbxa.mjs +328 -0
- package/dist/registry-DSd1GWB8.mjs +851 -0
- package/dist/request-context.d.mts +49 -0
- package/dist/request-context.mjs +42 -0
- package/dist/runner-B5l1JfOj.d.mts +26 -0
- package/dist/runner-BGUGywgG.mjs +1529 -0
- package/dist/runtime.d.mts +25 -0
- package/dist/runtime.mjs +41 -0
- package/dist/search-BNruJHDL.mjs +11054 -0
- package/dist/seed/index.d.mts +3 -0
- package/dist/seed/index.mjs +15 -0
- package/dist/seo/index.d.mts +69 -0
- package/dist/seo/index.mjs +69 -0
- package/dist/storage/local.d.mts +38 -0
- package/dist/storage/local.mjs +165 -0
- package/dist/storage/s3.d.mts +31 -0
- package/dist/storage/s3.mjs +174 -0
- package/dist/tokens-4vgYuXsZ.mjs +170 -0
- package/dist/transport-C5FYnid7.mjs +417 -0
- package/dist/transport-gIL-e43D.d.mts +41 -0
- package/dist/types-BawVha09.mjs +30 -0
- package/dist/types-BgQeVaPj.d.mts +192 -0
- package/dist/types-CLLdsG3g.d.mts +103 -0
- package/dist/types-D38djUXv.d.mts +1196 -0
- package/dist/types-DShnjzb6.mjs +15 -0
- package/dist/types-DkvMXalq.d.mts +425 -0
- package/dist/types-DuNbGKjF.mjs +74 -0
- package/dist/types-ju-_ORz7.d.mts +182 -0
- package/dist/validate-CXnRKfJK.mjs +327 -0
- package/dist/validate-CqRJb_xU.mjs +96 -0
- package/dist/validate-DVKJJ-M_.d.mts +377 -0
- package/locals.d.ts +47 -0
- package/package.json +313 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { encode } from "blurhash";
|
|
2
|
+
import { imageSize } from "image-size";
|
|
3
|
+
|
|
4
|
+
//#region src/media/normalize.ts
|
|
5
|
+
const INTERNAL_MEDIA_PREFIX = "/_dineway/api/media/file/";
|
|
6
|
+
const URL_PATTERN = /^https?:\/\//;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a media field value into a consistent MediaValue shape.
|
|
9
|
+
*
|
|
10
|
+
* - `null`/`undefined` → `null`
|
|
11
|
+
* - Bare URL string → `{ provider: "external", id: "", src: url }`
|
|
12
|
+
* - Bare internal media URL → resolved via local provider's `get()`
|
|
13
|
+
* - Object with `provider` + `id` → enriched with missing fields from provider
|
|
14
|
+
*/
|
|
15
|
+
async function normalizeMediaValue(value, getProvider) {
|
|
16
|
+
if (value == null) return null;
|
|
17
|
+
if (typeof value === "string") return normalizeStringUrl(value, getProvider);
|
|
18
|
+
if (!isRecord(value)) return null;
|
|
19
|
+
if (!("id" in value) && !("src" in value)) return null;
|
|
20
|
+
const provider = (typeof value.provider === "string" ? value.provider : void 0) || "local";
|
|
21
|
+
const id = typeof value.id === "string" ? value.id : "";
|
|
22
|
+
if (provider === "external") return recordToMediaValue(value);
|
|
23
|
+
const result = {
|
|
24
|
+
...recordToMediaValue(value),
|
|
25
|
+
provider
|
|
26
|
+
};
|
|
27
|
+
if (provider === "local") delete result.src;
|
|
28
|
+
const needsDimensions = result.width == null || result.height == null;
|
|
29
|
+
const needsStorageKey = provider === "local" && !result.meta?.storageKey;
|
|
30
|
+
const needsFileInfo = !result.mimeType || !result.filename;
|
|
31
|
+
if (!(needsDimensions || needsStorageKey || needsFileInfo) || !id) return result;
|
|
32
|
+
const mediaProvider = getProvider(provider);
|
|
33
|
+
if (!mediaProvider?.get) return result;
|
|
34
|
+
let providerItem;
|
|
35
|
+
try {
|
|
36
|
+
providerItem = await mediaProvider.get(id);
|
|
37
|
+
} catch {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
if (!providerItem) return result;
|
|
41
|
+
return mergeProviderData(result, providerItem);
|
|
42
|
+
}
|
|
43
|
+
function normalizeStringUrl(url, getProvider) {
|
|
44
|
+
if (url.startsWith(INTERNAL_MEDIA_PREFIX)) return resolveInternalUrl(url, getProvider);
|
|
45
|
+
if (URL_PATTERN.test(url)) return Promise.resolve({
|
|
46
|
+
provider: "external",
|
|
47
|
+
id: "",
|
|
48
|
+
src: url
|
|
49
|
+
});
|
|
50
|
+
return Promise.resolve({
|
|
51
|
+
provider: "external",
|
|
52
|
+
id: "",
|
|
53
|
+
src: url
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function resolveInternalUrl(url, getProvider) {
|
|
57
|
+
const storageKey = url.slice(25);
|
|
58
|
+
const localProvider = getProvider("local");
|
|
59
|
+
if (!localProvider?.get) return {
|
|
60
|
+
provider: "external",
|
|
61
|
+
id: "",
|
|
62
|
+
src: url
|
|
63
|
+
};
|
|
64
|
+
let item;
|
|
65
|
+
try {
|
|
66
|
+
item = await localProvider.get(storageKey);
|
|
67
|
+
} catch {
|
|
68
|
+
return {
|
|
69
|
+
provider: "external",
|
|
70
|
+
id: "",
|
|
71
|
+
src: url
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (!item) return {
|
|
75
|
+
provider: "external",
|
|
76
|
+
id: "",
|
|
77
|
+
src: url
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
provider: "local",
|
|
81
|
+
id: item.id,
|
|
82
|
+
filename: item.filename,
|
|
83
|
+
mimeType: item.mimeType,
|
|
84
|
+
width: item.width,
|
|
85
|
+
height: item.height,
|
|
86
|
+
alt: item.alt,
|
|
87
|
+
meta: item.meta
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Merge provider data into an existing MediaValue, preserving caller-supplied fields.
|
|
92
|
+
* Caller `alt` takes priority over provider `alt` (per-usage, not per-image).
|
|
93
|
+
*/
|
|
94
|
+
function mergeProviderData(existing, item) {
|
|
95
|
+
const result = { ...existing };
|
|
96
|
+
if (result.width == null && item.width != null) result.width = item.width;
|
|
97
|
+
if (result.height == null && item.height != null) result.height = item.height;
|
|
98
|
+
if (!result.filename && item.filename) result.filename = item.filename;
|
|
99
|
+
if (!result.mimeType && item.mimeType) result.mimeType = item.mimeType;
|
|
100
|
+
if (!result.alt && item.alt) result.alt = item.alt;
|
|
101
|
+
if (item.meta) result.meta = {
|
|
102
|
+
...item.meta,
|
|
103
|
+
...result.meta
|
|
104
|
+
};
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Extract known MediaValue fields from a runtime-checked record.
|
|
112
|
+
* Avoids unsafe `as MediaValue` cast by reading each property explicitly.
|
|
113
|
+
*/
|
|
114
|
+
function recordToMediaValue(obj) {
|
|
115
|
+
const result = { id: typeof obj.id === "string" ? obj.id : "" };
|
|
116
|
+
if (typeof obj.provider === "string") result.provider = obj.provider;
|
|
117
|
+
if (typeof obj.src === "string") result.src = obj.src;
|
|
118
|
+
if (typeof obj.previewUrl === "string") result.previewUrl = obj.previewUrl;
|
|
119
|
+
if (typeof obj.filename === "string") result.filename = obj.filename;
|
|
120
|
+
if (typeof obj.mimeType === "string") result.mimeType = obj.mimeType;
|
|
121
|
+
if (typeof obj.width === "number") result.width = obj.width;
|
|
122
|
+
if (typeof obj.height === "number") result.height = obj.height;
|
|
123
|
+
if (typeof obj.alt === "string") result.alt = obj.alt;
|
|
124
|
+
if (isRecord(obj.meta)) result.meta = obj.meta;
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/media/placeholder.ts
|
|
130
|
+
/**
|
|
131
|
+
* Image Placeholder Generation
|
|
132
|
+
*
|
|
133
|
+
* Generates blurhash and dominant color from image buffers for LQIP support.
|
|
134
|
+
* Decodes images via jpeg-js (pure JS) and upng-js (pure JS, uses pako for
|
|
135
|
+
* deflate). No Node-specific dependencies — works in Workers and Node SSR.
|
|
136
|
+
*/
|
|
137
|
+
const SUPPORTED_TYPES = {
|
|
138
|
+
"image/jpeg": "jpeg",
|
|
139
|
+
"image/jpg": "jpeg",
|
|
140
|
+
"image/png": "png"
|
|
141
|
+
};
|
|
142
|
+
/** Max width for blurhash input. Encode is O(w*h*components), so downsample first. */
|
|
143
|
+
const MAX_ENCODE_WIDTH = 32;
|
|
144
|
+
/** Max decoded RGBA size (32 MB). Images exceeding this skip placeholder generation. */
|
|
145
|
+
const MAX_DECODED_BYTES = 32 * 1024 * 1024;
|
|
146
|
+
/**
|
|
147
|
+
* Decode a JPEG buffer into raw RGBA pixel data.
|
|
148
|
+
*/
|
|
149
|
+
async function decodeJpeg(buffer) {
|
|
150
|
+
const { decode } = await import("jpeg-js");
|
|
151
|
+
const result = decode(buffer, { useTArray: true });
|
|
152
|
+
return {
|
|
153
|
+
width: result.width,
|
|
154
|
+
height: result.height,
|
|
155
|
+
data: result.data
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Decode a PNG buffer into raw RGBA pixel data.
|
|
160
|
+
* Uses upng-js (pure JS with pako deflate) — no Node zlib dependency.
|
|
161
|
+
*/
|
|
162
|
+
async function decodePng(buffer) {
|
|
163
|
+
const UPNG = (await import("upng-js")).default;
|
|
164
|
+
const img = UPNG.decode(buffer.buffer);
|
|
165
|
+
const frames = UPNG.toRGBA8(img);
|
|
166
|
+
const rgba = new Uint8Array(frames[0]);
|
|
167
|
+
return {
|
|
168
|
+
width: img.width,
|
|
169
|
+
height: img.height,
|
|
170
|
+
data: rgba
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Extract the dominant color from RGBA pixel data.
|
|
175
|
+
* Simple average of all non-transparent pixels.
|
|
176
|
+
*/
|
|
177
|
+
function extractDominantColor(data, width, height) {
|
|
178
|
+
let r = 0;
|
|
179
|
+
let g = 0;
|
|
180
|
+
let b = 0;
|
|
181
|
+
let count = 0;
|
|
182
|
+
const len = width * height * 4;
|
|
183
|
+
for (let i = 0; i < len; i += 4) {
|
|
184
|
+
if (data[i + 3] < 128) continue;
|
|
185
|
+
r += data[i];
|
|
186
|
+
g += data[i + 1];
|
|
187
|
+
b += data[i + 2];
|
|
188
|
+
count++;
|
|
189
|
+
}
|
|
190
|
+
if (count === 0) return "rgb(0,0,0)";
|
|
191
|
+
return `rgb(${Math.round(r / count)},${Math.round(g / count)},${Math.round(b / count)})`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Read image dimensions from headers without decoding pixel data.
|
|
195
|
+
*/
|
|
196
|
+
function getImageDimensions(buffer) {
|
|
197
|
+
try {
|
|
198
|
+
const result = imageSize(buffer);
|
|
199
|
+
if (result.width != null && result.height != null) return {
|
|
200
|
+
width: result.width,
|
|
201
|
+
height: result.height
|
|
202
|
+
};
|
|
203
|
+
return null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Generate blurhash and dominant color from an image buffer.
|
|
210
|
+
* Returns null for non-image MIME types or on failure.
|
|
211
|
+
*
|
|
212
|
+
* @param dimensions - Optional pre-known dimensions. Used as a fallback when
|
|
213
|
+
* image-size cannot parse the buffer (e.g. truncated headers). When the
|
|
214
|
+
* decoded size (width * height * 4) exceeds MAX_DECODED_BYTES, placeholder
|
|
215
|
+
* generation is skipped to avoid OOM on memory-constrained runtimes.
|
|
216
|
+
*/
|
|
217
|
+
async function generatePlaceholder(buffer, mimeType, dimensions) {
|
|
218
|
+
const format = SUPPORTED_TYPES[mimeType];
|
|
219
|
+
if (!format) return null;
|
|
220
|
+
try {
|
|
221
|
+
const dims = getImageDimensions(buffer) ?? dimensions;
|
|
222
|
+
if (dims && dims.width * dims.height * 4 > MAX_DECODED_BYTES) return null;
|
|
223
|
+
const { width, height, data } = format === "jpeg" ? await decodeJpeg(buffer) : await decodePng(buffer);
|
|
224
|
+
if (width === 0 || height === 0) return null;
|
|
225
|
+
let encodePixels;
|
|
226
|
+
let encodeWidth;
|
|
227
|
+
let encodeHeight;
|
|
228
|
+
if (width > MAX_ENCODE_WIDTH) {
|
|
229
|
+
const scale = MAX_ENCODE_WIDTH / width;
|
|
230
|
+
encodeWidth = MAX_ENCODE_WIDTH;
|
|
231
|
+
encodeHeight = Math.max(1, Math.round(height * scale));
|
|
232
|
+
encodePixels = downsample(data, width, height, encodeWidth, encodeHeight);
|
|
233
|
+
} else {
|
|
234
|
+
encodeWidth = width;
|
|
235
|
+
encodeHeight = height;
|
|
236
|
+
encodePixels = new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
blurhash: encode(encodePixels, encodeWidth, encodeHeight, 4, 3),
|
|
240
|
+
dominantColor: extractDominantColor(data, width, height)
|
|
241
|
+
};
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Nearest-neighbor downsample of RGBA pixel data.
|
|
248
|
+
*/
|
|
249
|
+
function downsample(src, srcW, srcH, dstW, dstH) {
|
|
250
|
+
const dst = new Uint8ClampedArray(dstW * dstH * 4);
|
|
251
|
+
for (let y = 0; y < dstH; y++) {
|
|
252
|
+
const srcY = Math.floor(y * srcH / dstH);
|
|
253
|
+
for (let x = 0; x < dstW; x++) {
|
|
254
|
+
const srcX = Math.floor(x * srcW / dstW);
|
|
255
|
+
const srcIdx = (srcY * srcW + srcX) * 4;
|
|
256
|
+
const dstIdx = (y * dstW + x) * 4;
|
|
257
|
+
dst[dstIdx] = src[srcIdx];
|
|
258
|
+
dst[dstIdx + 1] = src[srcIdx + 1];
|
|
259
|
+
dst[dstIdx + 2] = src[srcIdx + 2];
|
|
260
|
+
dst[dstIdx + 3] = src[srcIdx + 3];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return dst;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
export { normalizeMediaValue as n, generatePlaceholder as t };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
//#region src/media/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Media Provider Types
|
|
4
|
+
*
|
|
5
|
+
* Media providers are pluggable sources for browsing, uploading, and embedding media.
|
|
6
|
+
* They enable integration with external services (Unsplash, Cloudinary, Mux, etc.)
|
|
7
|
+
* alongside the built-in local media library.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Serializable media provider configuration descriptor
|
|
11
|
+
* Returned by provider config functions (e.g., unsplash(), mux())
|
|
12
|
+
*/
|
|
13
|
+
interface MediaProviderDescriptor<TConfig = Record<string, unknown>> {
|
|
14
|
+
/** Unique identifier, used in MediaValue.provider */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Display name for admin UI */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Icon for tab UI (emoji or URL) */
|
|
19
|
+
icon?: string;
|
|
20
|
+
/** Module path exporting createMediaProvider function */
|
|
21
|
+
entrypoint: string;
|
|
22
|
+
/** Optional React component module for custom admin UI */
|
|
23
|
+
adminModule?: string;
|
|
24
|
+
/** Capability flags determine UI behavior */
|
|
25
|
+
capabilities: MediaProviderCapabilities;
|
|
26
|
+
/** Serializable config passed to createMediaProvider at runtime */
|
|
27
|
+
config: TConfig;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Provider capabilities determine what UI elements to show
|
|
31
|
+
*/
|
|
32
|
+
interface MediaProviderCapabilities {
|
|
33
|
+
/** Can list/browse media */
|
|
34
|
+
browse: boolean;
|
|
35
|
+
/** Supports text search */
|
|
36
|
+
search: boolean;
|
|
37
|
+
/** Can upload new media */
|
|
38
|
+
upload: boolean;
|
|
39
|
+
/** Can delete media */
|
|
40
|
+
delete: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Options for listing media
|
|
44
|
+
*/
|
|
45
|
+
interface MediaListOptions {
|
|
46
|
+
/** Pagination cursor */
|
|
47
|
+
cursor?: string;
|
|
48
|
+
/** Max items to return (default 20) */
|
|
49
|
+
limit?: number;
|
|
50
|
+
/** Search query (if capabilities.search is true) */
|
|
51
|
+
query?: string;
|
|
52
|
+
/** Filter by MIME type prefix, e.g., "image/", "video/" */
|
|
53
|
+
mimeType?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Result from listing media
|
|
57
|
+
*/
|
|
58
|
+
interface MediaListResult {
|
|
59
|
+
items: MediaProviderItem[];
|
|
60
|
+
nextCursor?: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A media item as returned by a provider
|
|
64
|
+
* This is the provider's view of the item, before it's selected
|
|
65
|
+
*/
|
|
66
|
+
interface MediaProviderItem {
|
|
67
|
+
/** Provider-specific ID */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Original filename */
|
|
70
|
+
filename: string;
|
|
71
|
+
/** MIME type */
|
|
72
|
+
mimeType: string;
|
|
73
|
+
/** File size in bytes (if known) */
|
|
74
|
+
size?: number;
|
|
75
|
+
/** Dimensions (for images/video) */
|
|
76
|
+
width?: number;
|
|
77
|
+
height?: number;
|
|
78
|
+
/** Accessibility text */
|
|
79
|
+
alt?: string;
|
|
80
|
+
/** Preview URL for admin UI thumbnail */
|
|
81
|
+
previewUrl?: string;
|
|
82
|
+
/** Provider-specific metadata */
|
|
83
|
+
meta?: Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Input for uploading media
|
|
87
|
+
*/
|
|
88
|
+
interface MediaUploadInput {
|
|
89
|
+
file: File;
|
|
90
|
+
filename: string;
|
|
91
|
+
alt?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Options for generating embed
|
|
95
|
+
*/
|
|
96
|
+
interface EmbedOptions {
|
|
97
|
+
/** Desired width (provider may use for optimization) */
|
|
98
|
+
width?: number;
|
|
99
|
+
/** Desired height */
|
|
100
|
+
height?: number;
|
|
101
|
+
/** Image format preference */
|
|
102
|
+
format?: "webp" | "avif" | "jpeg" | "png" | "auto";
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Embed result types
|
|
106
|
+
*/
|
|
107
|
+
type EmbedResult = ImageEmbed | VideoEmbed | AudioEmbed | ComponentEmbed;
|
|
108
|
+
interface ImageEmbed {
|
|
109
|
+
type: "image";
|
|
110
|
+
src: string;
|
|
111
|
+
srcset?: string;
|
|
112
|
+
sizes?: string;
|
|
113
|
+
width?: number;
|
|
114
|
+
height?: number;
|
|
115
|
+
alt?: string;
|
|
116
|
+
/** Base URL without transforms, for responsive image generation */
|
|
117
|
+
cdnBaseUrl?: string;
|
|
118
|
+
/** For providers with URL-based transforms (Cloudinary, imgix) */
|
|
119
|
+
getSrc?: (opts: {
|
|
120
|
+
width?: number;
|
|
121
|
+
height?: number;
|
|
122
|
+
format?: string;
|
|
123
|
+
}) => string;
|
|
124
|
+
}
|
|
125
|
+
interface VideoEmbed {
|
|
126
|
+
type: "video";
|
|
127
|
+
/** Single source URL */
|
|
128
|
+
src?: string;
|
|
129
|
+
/** Multiple sources for format fallback */
|
|
130
|
+
sources?: Array<{
|
|
131
|
+
src: string;
|
|
132
|
+
type: string;
|
|
133
|
+
}>;
|
|
134
|
+
/** Poster/thumbnail image */
|
|
135
|
+
poster?: string;
|
|
136
|
+
width?: number;
|
|
137
|
+
height?: number;
|
|
138
|
+
/** Player controls */
|
|
139
|
+
controls?: boolean;
|
|
140
|
+
autoplay?: boolean;
|
|
141
|
+
muted?: boolean;
|
|
142
|
+
loop?: boolean;
|
|
143
|
+
playsinline?: boolean;
|
|
144
|
+
preload?: "none" | "metadata" | "auto";
|
|
145
|
+
crossorigin?: "anonymous" | "use-credentials";
|
|
146
|
+
}
|
|
147
|
+
interface AudioEmbed {
|
|
148
|
+
type: "audio";
|
|
149
|
+
src?: string;
|
|
150
|
+
sources?: Array<{
|
|
151
|
+
src: string;
|
|
152
|
+
type: string;
|
|
153
|
+
}>;
|
|
154
|
+
controls?: boolean;
|
|
155
|
+
autoplay?: boolean;
|
|
156
|
+
muted?: boolean;
|
|
157
|
+
loop?: boolean;
|
|
158
|
+
preload?: "none" | "metadata" | "auto";
|
|
159
|
+
}
|
|
160
|
+
interface ComponentEmbed {
|
|
161
|
+
type: "component";
|
|
162
|
+
/** Package to import from, e.g., "@mux/player-react" */
|
|
163
|
+
package: string;
|
|
164
|
+
/** Named export (default export if not specified) */
|
|
165
|
+
export?: string;
|
|
166
|
+
/** Props to pass to the component */
|
|
167
|
+
props: Record<string, unknown>;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Options for thumbnail generation
|
|
171
|
+
*/
|
|
172
|
+
interface ThumbnailOptions {
|
|
173
|
+
/** Desired width */
|
|
174
|
+
width?: number;
|
|
175
|
+
/** Desired height */
|
|
176
|
+
height?: number;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Runtime media provider interface
|
|
180
|
+
* Implemented by provider entrypoints
|
|
181
|
+
*/
|
|
182
|
+
interface MediaProvider {
|
|
183
|
+
/**
|
|
184
|
+
* List/search media items
|
|
185
|
+
*/
|
|
186
|
+
list(options: MediaListOptions): Promise<MediaListResult>;
|
|
187
|
+
/**
|
|
188
|
+
* Get a single item by ID (optional, for refresh/validation)
|
|
189
|
+
*/
|
|
190
|
+
get?(id: string): Promise<MediaProviderItem | null>;
|
|
191
|
+
/**
|
|
192
|
+
* Upload new media (if capabilities.upload is true)
|
|
193
|
+
*/
|
|
194
|
+
upload?(input: MediaUploadInput): Promise<MediaProviderItem>;
|
|
195
|
+
/**
|
|
196
|
+
* Delete media (if capabilities.delete is true)
|
|
197
|
+
*/
|
|
198
|
+
delete?(id: string): Promise<void>;
|
|
199
|
+
/**
|
|
200
|
+
* Get embed information for rendering this media item
|
|
201
|
+
* Called at runtime when rendering content
|
|
202
|
+
*/
|
|
203
|
+
getEmbed(value: MediaValue, options?: EmbedOptions): Promise<EmbedResult> | EmbedResult;
|
|
204
|
+
/**
|
|
205
|
+
* Get a thumbnail URL for admin display
|
|
206
|
+
* For images: returns a resized image URL
|
|
207
|
+
* For videos: returns a poster/thumbnail URL
|
|
208
|
+
*/
|
|
209
|
+
getThumbnailUrl?(id: string, mimeType?: string, options?: ThumbnailOptions): string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Function signature for provider entrypoint modules
|
|
213
|
+
*/
|
|
214
|
+
type CreateMediaProviderFn<TConfig = Record<string, unknown>> = (config: TConfig) => MediaProvider;
|
|
215
|
+
/**
|
|
216
|
+
* Media value stored in content fields
|
|
217
|
+
* This is what gets persisted when media is selected
|
|
218
|
+
*
|
|
219
|
+
* For backwards compatibility:
|
|
220
|
+
* - `provider` defaults to "local" if not specified
|
|
221
|
+
* - `src` is supported for legacy data or external URLs
|
|
222
|
+
*/
|
|
223
|
+
interface MediaValue {
|
|
224
|
+
/** Provider ID, e.g., "local", "unsplash", "mux" (defaults to "local") */
|
|
225
|
+
provider?: string;
|
|
226
|
+
/** Provider-specific item ID */
|
|
227
|
+
id: string;
|
|
228
|
+
/** Direct URL (for local media or legacy data) */
|
|
229
|
+
src?: string;
|
|
230
|
+
/** Preview URL for admin display (external providers) */
|
|
231
|
+
previewUrl?: string;
|
|
232
|
+
/** Cached metadata for display without runtime lookup */
|
|
233
|
+
filename?: string;
|
|
234
|
+
mimeType?: string;
|
|
235
|
+
width?: number;
|
|
236
|
+
height?: number;
|
|
237
|
+
alt?: string;
|
|
238
|
+
/** Provider-specific data needed for embedding */
|
|
239
|
+
meta?: Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Convert a MediaProviderItem to a MediaValue for storage
|
|
243
|
+
*/
|
|
244
|
+
declare function mediaItemToValue(providerId: string, item: MediaProviderItem): MediaValue;
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/media/normalize.d.ts
|
|
247
|
+
/**
|
|
248
|
+
* Normalize a media field value into a consistent MediaValue shape.
|
|
249
|
+
*
|
|
250
|
+
* - `null`/`undefined` → `null`
|
|
251
|
+
* - Bare URL string → `{ provider: "external", id: "", src: url }`
|
|
252
|
+
* - Bare internal media URL → resolved via local provider's `get()`
|
|
253
|
+
* - Object with `provider` + `id` → enriched with missing fields from provider
|
|
254
|
+
*/
|
|
255
|
+
declare function normalizeMediaValue(value: unknown, getProvider: (id: string) => MediaProvider | undefined): Promise<MediaValue | null>;
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/media/placeholder.d.ts
|
|
258
|
+
/**
|
|
259
|
+
* Image Placeholder Generation
|
|
260
|
+
*
|
|
261
|
+
* Generates blurhash and dominant color from image buffers for LQIP support.
|
|
262
|
+
* Decodes images via jpeg-js (pure JS) and upng-js (pure JS, uses pako for
|
|
263
|
+
* deflate). No Node-specific dependencies — works in Workers and Node SSR.
|
|
264
|
+
*/
|
|
265
|
+
interface PlaceholderData {
|
|
266
|
+
blurhash: string;
|
|
267
|
+
dominantColor: string;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Generate blurhash and dominant color from an image buffer.
|
|
271
|
+
* Returns null for non-image MIME types or on failure.
|
|
272
|
+
*
|
|
273
|
+
* @param dimensions - Optional pre-known dimensions. Used as a fallback when
|
|
274
|
+
* image-size cannot parse the buffer (e.g. truncated headers). When the
|
|
275
|
+
* decoded size (width * height * 4) exceeds MAX_DECODED_BYTES, placeholder
|
|
276
|
+
* generation is skipped to avoid OOM on memory-constrained runtimes.
|
|
277
|
+
*/
|
|
278
|
+
declare function generatePlaceholder(buffer: Uint8Array, mimeType: string, dimensions?: {
|
|
279
|
+
width: number;
|
|
280
|
+
height: number;
|
|
281
|
+
}): Promise<PlaceholderData | null>;
|
|
282
|
+
//#endregion
|
|
283
|
+
export { MediaValue as _, ComponentEmbed as a, mediaItemToValue as b, EmbedResult as c, MediaListResult as d, MediaProvider as f, MediaUploadInput as g, MediaProviderItem as h, AudioEmbed as i, ImageEmbed as l, MediaProviderDescriptor as m, generatePlaceholder as n, CreateMediaProviderFn as o, MediaProviderCapabilities as p, normalizeMediaValue as r, EmbedOptions as s, PlaceholderData as t, MediaListOptions as u, ThumbnailOptions as v, VideoEmbed as y };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
//#region src/plugin-utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for plugin admin UIs.
|
|
4
|
+
*
|
|
5
|
+
* Plugin admin components (`admin.tsx`) run inside the Dineway admin dashboard.
|
|
6
|
+
* This module provides the common helpers they all need: API fetching with CSRF
|
|
7
|
+
* protection, response envelope unwrapping, and type narrowing.
|
|
8
|
+
*
|
|
9
|
+
* Import as: `import { apiFetch, parseApiResponse, isRecord } from "dineway/plugin-utils";`
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Fetch wrapper that adds the `X-Dineway-Request` CSRF protection header.
|
|
13
|
+
*
|
|
14
|
+
* All plugin admin API calls should use this instead of raw `fetch()`.
|
|
15
|
+
* State-changing endpoints reject requests without this header.
|
|
16
|
+
*/
|
|
17
|
+
declare function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
18
|
+
/**
|
|
19
|
+
* Parse an API response, unwrapping the `{ data: T }` envelope.
|
|
20
|
+
*
|
|
21
|
+
* All plugin API routes return success responses wrapped in `{ data: ... }`
|
|
22
|
+
* by `apiSuccess()`. This helper unwraps that envelope and handles errors.
|
|
23
|
+
*
|
|
24
|
+
* On error responses (non-2xx), throws an Error with the server's message
|
|
25
|
+
* (from `{ error: { message } }`) or the fallback message.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const res = await apiFetch("/_dineway/api/plugins/my-plugin/items");
|
|
30
|
+
* const { items } = await parseApiResponse<{ items: Item[] }>(res, "Failed to load items");
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function parseApiResponse<T>(response: Response, fallbackMessage?: string): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Extract the error message from a failed API response.
|
|
36
|
+
*
|
|
37
|
+
* Error responses use the shape `{ error: { code, message } }`. This helper
|
|
38
|
+
* parses that body and returns the message, falling back to the provided default.
|
|
39
|
+
* Swallows JSON parse failures gracefully.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* if (!res.ok) {
|
|
44
|
+
* setError(await getErrorMessage(res, "Failed to save"));
|
|
45
|
+
* return;
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function getErrorMessage(response: Response, fallback: string): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Narrow `unknown` to a plain object record.
|
|
52
|
+
*
|
|
53
|
+
* Useful for safely inspecting untyped API responses before accessing properties.
|
|
54
|
+
*/
|
|
55
|
+
declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { apiFetch, getErrorMessage, isRecord, parseApiResponse };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/plugin-utils.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared utilities for plugin admin UIs.
|
|
4
|
+
*
|
|
5
|
+
* Plugin admin components (`admin.tsx`) run inside the Dineway admin dashboard.
|
|
6
|
+
* This module provides the common helpers they all need: API fetching with CSRF
|
|
7
|
+
* protection, response envelope unwrapping, and type narrowing.
|
|
8
|
+
*
|
|
9
|
+
* Import as: `import { apiFetch, parseApiResponse, isRecord } from "dineway/plugin-utils";`
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Fetch wrapper that adds the `X-Dineway-Request` CSRF protection header.
|
|
13
|
+
*
|
|
14
|
+
* All plugin admin API calls should use this instead of raw `fetch()`.
|
|
15
|
+
* State-changing endpoints reject requests without this header.
|
|
16
|
+
*/
|
|
17
|
+
function apiFetch(input, init) {
|
|
18
|
+
const headers = new Headers(init?.headers);
|
|
19
|
+
headers.set("X-Dineway-Request", "1");
|
|
20
|
+
return fetch(input, {
|
|
21
|
+
...init,
|
|
22
|
+
headers
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse an API response, unwrapping the `{ data: T }` envelope.
|
|
27
|
+
*
|
|
28
|
+
* All plugin API routes return success responses wrapped in `{ data: ... }`
|
|
29
|
+
* by `apiSuccess()`. This helper unwraps that envelope and handles errors.
|
|
30
|
+
*
|
|
31
|
+
* On error responses (non-2xx), throws an Error with the server's message
|
|
32
|
+
* (from `{ error: { message } }`) or the fallback message.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const res = await apiFetch("/_dineway/api/plugins/my-plugin/items");
|
|
37
|
+
* const { items } = await parseApiResponse<{ items: Item[] }>(res, "Failed to load items");
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
async function parseApiResponse(response, fallbackMessage = "Request failed") {
|
|
41
|
+
if (!response.ok) throw new Error(await getErrorMessage(response, `${fallbackMessage}: ${response.statusText}`));
|
|
42
|
+
return (await response.json()).data;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract the error message from a failed API response.
|
|
46
|
+
*
|
|
47
|
+
* Error responses use the shape `{ error: { code, message } }`. This helper
|
|
48
|
+
* parses that body and returns the message, falling back to the provided default.
|
|
49
|
+
* Swallows JSON parse failures gracefully.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* if (!res.ok) {
|
|
54
|
+
* setError(await getErrorMessage(res, "Failed to save"));
|
|
55
|
+
* return;
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
async function getErrorMessage(response, fallback) {
|
|
60
|
+
const body = await response.json().catch(() => ({}));
|
|
61
|
+
if (isRecord(body) && isRecord(body.error)) {
|
|
62
|
+
const msg = body.error.message;
|
|
63
|
+
if (typeof msg === "string") return msg;
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Narrow `unknown` to a plain object record.
|
|
69
|
+
*
|
|
70
|
+
* Useful for safely inspecting untyped API responses before accessing properties.
|
|
71
|
+
*/
|
|
72
|
+
function isRecord(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { apiFetch, getErrorMessage, isRecord, parseApiResponse };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "../types-DkvMXalq.mjs";
|
|
2
|
+
import { wn as PluginDescriptor } from "../index-C-jx21qs.mjs";
|
|
3
|
+
import "../runner-B5l1JfOj.mjs";
|
|
4
|
+
import { $ as StandardPluginDefinition, J as ResolvedPlugin } from "../types-D38djUXv.mjs";
|
|
5
|
+
import "../validate-DVKJJ-M_.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/plugins/adapt-sandbox-entry.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Adapt a standard-format plugin definition into a ResolvedPlugin.
|
|
10
|
+
*
|
|
11
|
+
* This is the core of the unified plugin format. It takes the `{ hooks, routes }`
|
|
12
|
+
* export from a standard plugin and produces a ResolvedPlugin that can enter the
|
|
13
|
+
* HookPipeline alongside native plugins.
|
|
14
|
+
*
|
|
15
|
+
* @param definition - The standard plugin definition (from definePlugin() or raw export)
|
|
16
|
+
* @param descriptor - The plugin descriptor with id, version, capabilities, etc.
|
|
17
|
+
* @returns A ResolvedPlugin compatible with HookPipeline
|
|
18
|
+
*/
|
|
19
|
+
declare function adaptSandboxEntry(definition: StandardPluginDefinition, descriptor: PluginDescriptor): ResolvedPlugin;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { adaptSandboxEntry };
|