@cocreate/file-server 1.18.8 → 1.18.10
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/index.js +112 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [1.18.10](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.9...v1.18.10) (2025-11-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* enhance font and data URI handling in file processing ([25b7e8c](https://github.com/CoCreate-app/CoCreate-file-server/commit/25b7e8c1976c12ea99f1806025c248e0fa865ef0))
|
|
7
|
+
|
|
8
|
+
## [1.18.9](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.8...v1.18.9) (2025-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* add theme detection and handling for preferred color scheme in responses ([484a37c](https://github.com/CoCreate-app/CoCreate-file-server/commit/484a37c3f35b397a3baff0d9dc62c338a0135903))
|
|
14
|
+
|
|
1
15
|
## [1.18.8](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.7...v1.18.8) (2025-10-11)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -30,6 +30,24 @@ class CoCreateFileSystem {
|
|
|
30
30
|
try {
|
|
31
31
|
const hostname = urlObject.hostname;
|
|
32
32
|
|
|
33
|
+
// --- determine preferred color scheme from browser headers ---
|
|
34
|
+
let theme;
|
|
35
|
+
const headerValue =
|
|
36
|
+
(req && req.headers && (
|
|
37
|
+
req.headers["sec-ch-prefers-color-scheme"] ||
|
|
38
|
+
req.headers["prefers-color-scheme"] ||
|
|
39
|
+
req.headers["x-prefers-color-scheme"] ||
|
|
40
|
+
req.headers["x-color-scheme"]
|
|
41
|
+
));
|
|
42
|
+
if (typeof headerValue === "string") {
|
|
43
|
+
// take first token, strip quotes, normalize
|
|
44
|
+
const token = headerValue.split(",")[0].trim().replace(/^"|"$/g, "").toLowerCase();
|
|
45
|
+
if (token === "dark" || token === "light") theme = token;
|
|
46
|
+
}
|
|
47
|
+
// optional: expose detected theme to clients / downstream code
|
|
48
|
+
if (theme) res.setHeader("X-Preferred-Color-Scheme", theme);
|
|
49
|
+
// --- end theme detection ---
|
|
50
|
+
|
|
33
51
|
let data = {
|
|
34
52
|
method: "object.read",
|
|
35
53
|
host: hostname,
|
|
@@ -199,67 +217,103 @@ class CoCreateFileSystem {
|
|
|
199
217
|
|
|
200
218
|
let contentType = file["content-type"] || "text/html";
|
|
201
219
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
// Normalize and decode src based on content-type and pathname.
|
|
221
|
+
async function normalizeSrc(src, contentType, pathname) {
|
|
222
|
+
const isBase64Only = (s) =>
|
|
223
|
+
typeof s === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(s) && s.length % 4 === 0;
|
|
224
|
+
|
|
225
|
+
const parseDataUri = (s) => {
|
|
226
|
+
// returns { mime, isBase64, data } or null
|
|
227
|
+
const m = /^data:([^;]+)(;base64)?,(.*)$/is.exec(s);
|
|
228
|
+
if (!m) return null;
|
|
229
|
+
return { mime: m[1].toLowerCase(), isBase64: !!m[2], data: m[3] };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Fonts: data:font/...;base64 or plain base64 -> Buffer
|
|
233
|
+
if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
|
|
234
|
+
if (Buffer.isBuffer(src)) return src;
|
|
235
|
+
if (typeof src !== "string") throw new Error("Invalid font src");
|
|
236
|
+
const d = parseDataUri(src);
|
|
237
|
+
if (d && d.isBase64) return Buffer.from(d.data, "base64");
|
|
238
|
+
// maybe stored as bare base64
|
|
239
|
+
if (isBase64Only(src)) return Buffer.from(src, "base64");
|
|
240
|
+
throw new Error("Font data is not valid base64 or data URI");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Data URIs
|
|
244
|
+
if (typeof src === "string") {
|
|
245
|
+
const d = parseDataUri(src);
|
|
246
|
+
if (d) {
|
|
247
|
+
// SVG: decode to utf8 string so browsers render SVG correctly
|
|
248
|
+
if (d.mime === "image/svg+xml") {
|
|
249
|
+
if (d.isBase64) return Buffer.from(d.data, "base64").toString("utf8");
|
|
250
|
+
// URI-encoded SVG payload
|
|
251
|
+
try {
|
|
252
|
+
return decodeURIComponent(d.data);
|
|
253
|
+
} catch (_) {
|
|
254
|
+
return d.data;
|
|
255
|
+
}
|
|
208
256
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
257
|
+
// Raster images -> Buffer
|
|
258
|
+
if (/^image\/(png|jpe?g|webp|bmp|gif)$/i.test(d.mime)) {
|
|
259
|
+
if (d.isBase64) return Buffer.from(d.data, "base64");
|
|
260
|
+
}
|
|
261
|
+
// If it's a text data URI (e.g., xml) and not base64, return decoded text
|
|
262
|
+
if (!d.isBase64) {
|
|
263
|
+
try {
|
|
264
|
+
return decodeURIComponent(d.data);
|
|
265
|
+
} catch (_) {
|
|
266
|
+
return d.data;
|
|
267
|
+
}
|
|
214
268
|
}
|
|
215
|
-
} else {
|
|
216
|
-
throw new Error("Font data is not a valid base64 string");
|
|
217
269
|
}
|
|
218
|
-
} catch (err) {
|
|
219
|
-
console.error("Error processing font file:", {
|
|
220
|
-
message: err.message,
|
|
221
|
-
contentType,
|
|
222
|
-
srcType: typeof src
|
|
223
|
-
});
|
|
224
|
-
let pageNotFound = await getDefaultFile("/404.html");
|
|
225
|
-
return sendResponse(pageNotFound.object[0].src, 404, {
|
|
226
|
-
"Content-Type": "text/html"
|
|
227
|
-
});
|
|
228
270
|
}
|
|
229
|
-
} else if (
|
|
230
|
-
/^data:image\/[a-zA-Z0-9+.-]+;base64,([A-Za-z0-9+/]+={0,2})$/.test(
|
|
231
|
-
src
|
|
232
|
-
)
|
|
233
|
-
) {
|
|
234
|
-
src = src.replace(/^data:image\/(png|jpeg|jpg);base64,/, "");
|
|
235
|
-
src = Buffer.from(src, "base64");
|
|
236
|
-
} else if (/^([A-Za-z0-9+/]+={0,2})$/.test(src)) {
|
|
237
|
-
src = Buffer.from(src, "base64");
|
|
238
|
-
} else if (contentType === "text/html") {
|
|
239
|
-
try {
|
|
240
|
-
let data = {};
|
|
241
271
|
|
|
242
|
-
|
|
272
|
+
// Plain base64-only string: decode to Buffer only for binary content types
|
|
273
|
+
if (isBase64Only(src)) {
|
|
274
|
+
if (
|
|
275
|
+
contentType.startsWith("image/") ||
|
|
276
|
+
contentType.startsWith("font/") ||
|
|
277
|
+
contentType === "application/octet-stream"
|
|
278
|
+
) {
|
|
279
|
+
return Buffer.from(src, "base64");
|
|
280
|
+
}
|
|
281
|
+
// textual content kept as-is
|
|
282
|
+
return src;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// HTML rendering
|
|
286
|
+
if (contentType === "text/html") {
|
|
287
|
+
return await this.render.HTML(
|
|
243
288
|
file,
|
|
244
289
|
organization,
|
|
245
290
|
urlObject,
|
|
246
291
|
langRegion,
|
|
247
|
-
lang
|
|
292
|
+
lang,
|
|
293
|
+
theme
|
|
248
294
|
);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
console.warn("server-side-render: " + err.message);
|
|
251
295
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
contentType === "application/xml"
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
|
|
297
|
+
// XML host replacement if src is string
|
|
298
|
+
if ((contentType === "text/xml" || contentType === "application/xml") && typeof src === "string") {
|
|
299
|
+
const protocol = "https://";
|
|
300
|
+
return src.replaceAll("{{$host}}", `${protocol}${hostname}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Otherwise return as-is (string, Buffer, object)
|
|
304
|
+
return src;
|
|
261
305
|
}
|
|
262
306
|
|
|
307
|
+
try {
|
|
308
|
+
src = await normalizeSrc.call(this, src, contentType, pathname);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
console.error("Error processing file src:", err && err.message);
|
|
311
|
+
let pageNotFound = await getDefaultFile("/404.html");
|
|
312
|
+
return sendResponse(pageNotFound.object[0].src, 404, {
|
|
313
|
+
"Content-Type": "text/html"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
263
317
|
sendResponse(src, 200, { "Content-Type": contentType });
|
|
264
318
|
this.sitemap.check(file, hostname);
|
|
265
319
|
|
|
@@ -289,6 +343,16 @@ class CoCreateFileSystem {
|
|
|
289
343
|
|
|
290
344
|
// Always override/set Cache-Control header so the response aligns with the file metadata/defaults
|
|
291
345
|
headers["Cache-Control"] = cacheControl;
|
|
346
|
+
// Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
|
|
347
|
+
// After the browser sees this in a response it may include
|
|
348
|
+
// the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
|
|
349
|
+
headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
|
|
350
|
+
// optional: tell browser how long to remember this preference (seconds)
|
|
351
|
+
headers["Accept-CH-Lifetime"] = "86400";
|
|
352
|
+
// ensure caches/proxies vary responses by this hint
|
|
353
|
+
headers["Vary"] = headers["Vary"]
|
|
354
|
+
? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
|
|
355
|
+
: "Sec-CH-Prefers-Color-Scheme";
|
|
292
356
|
|
|
293
357
|
if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
|
|
294
358
|
// Ensure binary data is sent as-is
|