@cocreate/file-server 1.18.10 → 1.19.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.
@@ -22,13 +22,13 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
24
  - name: Checkout
25
- uses: actions/checkout@v3
25
+ uses: actions/checkout@v4
26
26
  - name: Setup Node.js
27
- uses: actions/setup-node@v3
27
+ uses: actions/setup-node@v4
28
28
  with:
29
- node-version: 14
29
+ node-version: 22 # Required for the latest semantic-release plugins
30
30
  - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
31
+ uses: cycjimmy/semantic-release-action@v4 # Update to v4 for better Node 20+ support
32
32
  id: semantic
33
33
  with:
34
34
  extra_plugins: |
@@ -36,7 +36,7 @@ jobs:
36
36
  @semantic-release/git
37
37
  @semantic-release/github
38
38
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB }}"
39
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Use the built-in token if possible
40
40
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
41
41
  outputs:
42
42
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [1.19.0](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.10...v1.19.0) (2026-04-08)
2
+
3
+
4
+ ### Features
5
+
6
+ * support ico images ([cd9720d](https://github.com/CoCreate-app/CoCreate-file-server/commit/cd9720df7c534728a5732ea4bd44e9ce85ed7a74))
7
+
1
8
  ## [1.18.10](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.9...v1.18.10) (2025-11-17)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/file-server",
3
- "version": "1.18.10",
3
+ "version": "1.19.0",
4
4
  "description": "A simple file-server component in vanilla javascript. Easily configured using HTML5 data-attributes and/or JavaScript API.",
5
5
  "keywords": [
6
6
  "file-server",
package/src/index.js CHANGED
@@ -21,463 +21,464 @@
21
21
  // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
22
 
23
23
  class CoCreateFileSystem {
24
- constructor(render, sitemap) {
25
- this.render = render;
26
- this.sitemap = sitemap;
27
- }
28
-
29
- async send(req, res, crud, organization, urlObject) {
30
- try {
31
- const hostname = urlObject.hostname;
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
-
51
- let data = {
52
- method: "object.read",
53
- host: hostname,
54
- array: "files",
55
- $filter: {
56
- query: {
57
- host: { $in: [hostname, "*"] }
58
- },
59
- limit: 1
60
- }
61
- };
62
-
63
- let organization_id;
64
- if (!organization || organization.error) {
65
- let hostNotFound = await getDefaultFile("/hostNotFound.html");
66
- return sendResponse(hostNotFound.object[0].src, 404, {
67
- "Content-Type": "text/html"
68
- });
69
- }
70
-
71
- organization_id = organization._id;
72
- data.organization_id = organization_id;
73
-
74
- res.setHeader("organization", organization_id);
75
- res.setHeader("storage", !!organization.storage);
76
- res.setHeader("Access-Control-Allow-Origin", "*");
77
- res.setHeader("Access-Control-Allow-Methods", "");
78
- res.setHeader(
79
- "Access-Control-Allow-Headers",
80
- "Content-Type, Authorization"
81
- );
82
-
83
- let active = crud.wsManager.organizations.get(organization_id);
84
- if (active === false) {
85
- let balanceFalse = await getDefaultFile("/balanceFalse.html");
86
- return sendResponse(balanceFalse.object[0].src, 403, {
87
- "Content-Type": "text/html",
88
- "Account-Balance": "false",
89
- storage: organization.storage
90
- });
91
- }
92
-
93
- let parameters = urlObject.searchParams;
94
- if (parameters.size) {
95
- console.log("parameters", parameters);
96
- }
97
-
98
- let pathname = urlObject.pathname;
99
-
100
- if (pathname.endsWith("/")) {
101
- pathname += "index.html";
102
- } else if (!pathname.startsWith("/.well-known/acme-challenge")) {
103
- let directory = pathname.split("/").slice(-1)[0];
104
- if (!directory.includes(".")) pathname += "/index.html";
105
- }
106
-
107
- // Match both /en/ and /en-US/ style URLs
108
- const bcp47Regex = /^\/([a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})?)\//;
109
- const langMatch = pathname.match(bcp47Regex);
110
- let lang, langRegion;
111
- if (langMatch) {
112
- langRegion = langMatch[1];
113
- lang = langRegion.split("-")[0]; // Get just the base language (e.g., 'en', 'es', 'fr')
114
- let basePathname = pathname.replace("/" + langRegion, "");
115
- data.$filter.query.pathname = basePathname;
116
- } else {
117
- // No language in URL, try Accept-Language header
118
- let acceptLang = req.headers && req.headers["accept-language"];
119
- if (acceptLang) {
120
- // Parse the Accept-Language header, get the first language
121
- let preferred = acceptLang.split(",")[0].trim();
122
- if (preferred) {
123
- lang = preferred.split("-")[0];
124
- langRegion = preferred;
125
- // --- BEGIN OPTIONAL REDIRECT ---
126
- // Uncomment to enable automatic redirect to language-specific path
127
- // let newPath = `/${preferred}${pathname.startsWith('/') ? '' : '/'}${pathname.replace(/^\//, '')}`;
128
- // res.writeHead(302, { Location: newPath });
129
- // return res.end();
130
- // --- END OPTIONAL REDIRECT ---
131
- }
132
- }
133
- data.$filter.query.pathname = pathname;
134
- }
135
-
136
- let file;
137
- if (
138
- pathname.startsWith("/dist") ||
139
- pathname.startsWith("/admin") ||
140
- [
141
- "/403.html",
142
- "/404.html",
143
- "/offline.html",
144
- "/manifest.webmanifest",
145
- "/service-worker.js"
146
- ].includes(pathname)
147
- ) {
148
- file = await getDefaultFile(pathname);
149
- } else {
150
- file = await crud.send(data);
151
- }
152
-
153
- // --- Wildcard fallback ---
154
- if (!file || !file.object || !file.object[0]) {
155
- pathname = urlObject.pathname;
156
- let lastIndex = pathname.lastIndexOf("/");
157
- let wildcardPath = pathname.substring(0, lastIndex + 1);
158
- let wildcard = pathname.substring(lastIndex + 1);
159
-
160
- if (wildcard.includes(".")) {
161
- let fileLastIndex = wildcard.lastIndexOf(".");
162
- let fileExtension = wildcard.substring(fileLastIndex);
163
- wildcard = wildcardPath + "*" + fileExtension; // Create wildcard for file name
164
- } else {
165
- wildcard = wildcardPath + "*/index.html"; // Append '*' if it's just a path or folder
166
- }
167
-
168
- data.$filter.query.pathname = wildcard;
169
- file = await crud.send(data);
170
- }
171
-
172
- if (!file || !file.object || !file.object[0]) {
173
- let pageNotFound = await getDefaultFile("/404.html");
174
- return sendResponse(pageNotFound.object[0].src, 404, {
175
- "Content-Type": "text/html"
176
- });
177
- }
178
-
179
- file = file.object[0];
180
- if (!file["public"] || file["public"] === "false") {
181
- let pageForbidden = await getDefaultFile("/403.html");
182
- return sendResponse(pageForbidden.object[0].src, 403, {
183
- "Content-Type": "text/html"
184
- });
185
- }
186
-
187
- let src;
188
- if (file["src"]) {
189
- src = file["src"];
190
- } else {
191
- let fileSrc = await crud.send({
192
- method: "object.read",
193
- host: hostname,
194
- array: file["array"],
195
- object: {
196
- _id: file._id
197
- },
198
- organization_id
199
- });
200
- src = fileSrc[file["name"]];
201
- }
202
-
203
- if (!src) {
204
- let pageNotFound = await getDefaultFile("/404.html");
205
- return sendResponse(pageNotFound.object[0].src, 404, {
206
- "Content-Type": "text/html"
207
- });
208
- }
209
-
210
- let modifiedOn = file.modified || file.created;
211
- if (modifiedOn) {
212
- modifiedOn = modifiedOn.on;
213
- if (modifiedOn instanceof Date)
214
- modifiedOn = modifiedOn.toISOString();
215
- res.setHeader("Last-Modified", modifiedOn);
216
- }
217
-
218
- let contentType = file["content-type"] || "text/html";
219
-
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
- }
256
- }
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
- }
268
- }
269
- }
270
- }
271
-
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(
288
- file,
289
- organization,
290
- urlObject,
291
- langRegion,
292
- lang,
293
- theme
294
- );
295
- }
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;
305
- }
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
-
317
- sendResponse(src, 200, { "Content-Type": contentType });
318
- this.sitemap.check(file, hostname);
319
-
320
- function sendResponse(src, statusCode, headers) {
321
- try {
322
- let cacheControl;
323
- if (statusCode >= 400) {
324
- cacheControl = "no-cache, no-store, must-revalidate";
325
- } else if (
326
- file &&
327
- file["cache-control"] !== undefined &&
328
- file["cache-control"] !== null
329
- ) {
330
- const val = file["cache-control"];
331
- if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
332
- // If it's numeric (number or numeric string) treat it as max-age.
333
- cacheControl = `public, max-age=${String(
334
- val
335
- ).trim()};`;
336
- } else {
337
- // use the value verbatim (allow full Cache-Control strings like "no-cache" or "public, max-age=600")
338
- cacheControl = val;
339
- }
340
- } else {
341
- cacheControl = organization["cache-control"] || "public, max-age=3600";
342
- }
343
-
344
- // Always override/set Cache-Control header so the response aligns with the file metadata/defaults
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";
356
-
357
- if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
358
- // Ensure binary data is sent as-is
359
- } else if (typeof src === "object") {
360
- src = JSON.stringify(src);
361
- }
362
-
363
- if (organization_id)
364
- crud.wsManager.emit("setBandwidth", {
365
- type: "out",
366
- data: src,
367
- organization_id
368
- });
369
- res.writeHead(statusCode, headers);
370
- return res.end(src);
371
- } catch (error) {
372
- console.log(error);
373
- }
374
- }
375
-
376
- async function getDefaultFile(fileName) {
377
- data.$filter.query.pathname = fileName;
378
- // data.$filter.query.$or[0] = { pathname: fileName }
379
- let defaultFile;
380
- if (fileName !== "/hostNotFound.html")
381
- defaultFile = await crud.send(data);
382
-
383
- if (
384
- defaultFile &&
385
- defaultFile.object &&
386
- defaultFile.object[0] &&
387
- defaultFile.object[0].src
388
- ) {
389
- return defaultFile;
390
- } else {
391
- data.$filter.query.host.$in = ["*"];
392
- data.organization_id = process.env.organization_id;
393
-
394
- if (fileName.startsWith("/admin"))
395
- data.$filter.query.pathname =
396
- "/superadmin" + fileName.replace("/admin", "");
397
-
398
- defaultFile = await crud.send(data);
399
-
400
- if (fileName !== "/hostNotFound.html") {
401
- crud.wsManager.emit("setBandwidth", {
402
- type: "out",
403
- data,
404
- organization_id
405
- });
406
-
407
- crud.wsManager.emit("setBandwidth", {
408
- type: "in",
409
- data: defaultFile,
410
- organization_id
411
- });
412
- }
413
-
414
- if (
415
- defaultFile &&
416
- defaultFile.object &&
417
- defaultFile.object[0] &&
418
- defaultFile.object[0].src
419
- ) {
420
- if (fileName.startsWith("/admin")) {
421
- data.object[0].directory = "admin";
422
- data.object[0].path =
423
- "/admin" +
424
- data.object[0].path.replace("/superadmin", "");
425
- data.object[0].pathname = fileName;
426
- }
427
-
428
- crud.send({
429
- method: "object.create",
430
- host: hostname,
431
- array: "files",
432
- object: defaultFile.object[0],
433
- organization_id
434
- });
435
-
436
- return defaultFile;
437
- } else {
438
- switch (fileName) {
439
- case "/403.html":
440
- defaultFile.object = [
441
- {
442
- src: `${pathname} access not allowed for ${organization_id}`
443
- }
444
- ];
445
- break;
446
- case "/404.html":
447
- defaultFile.object = [
448
- {
449
- src: `${pathname} could not be found for ${organization_id}`
450
- }
451
- ];
452
- break;
453
- case "/balanceFalse.html":
454
- defaultFile.object = [
455
- {
456
- src: "This organizations account balance has fallen bellow 0: "
457
- }
458
- ];
459
- break;
460
- case "/hostNotFound.html":
461
- defaultFile.object = [
462
- {
463
- src:
464
- "An organization could not be found using the host: " +
465
- hostname +
466
- " in platformDB: " +
467
- process.env.organization_id
468
- }
469
- ];
470
- break;
471
- }
472
- return defaultFile;
473
- }
474
- }
475
- }
476
- } catch (error) {
477
- res.writeHead(400, { "Content-Type": "text/plain" });
478
- res.end("Invalid host format");
479
- }
480
- }
24
+ constructor(render, sitemap) {
25
+ this.render = render;
26
+ this.sitemap = sitemap;
27
+ }
28
+
29
+ async send(req, res, crud, organization, urlObject) {
30
+ try {
31
+ const hostname = urlObject.hostname;
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
+
51
+ let data = {
52
+ method: "object.read",
53
+ host: hostname,
54
+ array: "files",
55
+ $filter: {
56
+ query: {
57
+ host: { $in: [hostname, "*"] }
58
+ },
59
+ limit: 1
60
+ }
61
+ };
62
+
63
+ let organization_id;
64
+ if (!organization || organization.error) {
65
+ let hostNotFound = await getDefaultFile("/hostNotFound.html");
66
+ return sendResponse(hostNotFound.object[0].src, 404, {
67
+ "Content-Type": "text/html"
68
+ });
69
+ }
70
+
71
+ organization_id = organization._id;
72
+ data.organization_id = organization_id;
73
+
74
+ res.setHeader("organization", organization_id);
75
+ res.setHeader("storage", !!organization.storage);
76
+ res.setHeader("Access-Control-Allow-Origin", "*");
77
+ res.setHeader("Access-Control-Allow-Methods", "");
78
+ res.setHeader(
79
+ "Access-Control-Allow-Headers",
80
+ "Content-Type, Authorization"
81
+ );
82
+
83
+ let active = crud.wsManager.organizations.get(organization_id);
84
+ if (active === false) {
85
+ let balanceFalse = await getDefaultFile("/balanceFalse.html");
86
+ return sendResponse(balanceFalse.object[0].src, 403, {
87
+ "Content-Type": "text/html",
88
+ "Account-Balance": "false",
89
+ storage: organization.storage
90
+ });
91
+ }
92
+
93
+ let parameters = urlObject.searchParams;
94
+ if (parameters.size) {
95
+ console.log("parameters", parameters);
96
+ }
97
+
98
+ let pathname = urlObject.pathname;
99
+
100
+ if (pathname.endsWith("/")) {
101
+ pathname += "index.html";
102
+ } else if (!pathname.startsWith("/.well-known/acme-challenge")) {
103
+ let directory = pathname.split("/").slice(-1)[0];
104
+ if (!directory.includes(".")) pathname += "/index.html";
105
+ }
106
+
107
+ // Match both /en/ and /en-US/ style URLs
108
+ const bcp47Regex = /^\/([a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})?)\//;
109
+ const langMatch = pathname.match(bcp47Regex);
110
+ let lang, langRegion;
111
+ if (langMatch) {
112
+ langRegion = langMatch[1];
113
+ lang = langRegion.split("-")[0]; // Get just the base language (e.g., 'en', 'es', 'fr')
114
+ let basePathname = pathname.replace("/" + langRegion, "");
115
+ data.$filter.query.pathname = basePathname;
116
+ } else {
117
+ // No language in URL, try Accept-Language header
118
+ let acceptLang = req.headers && req.headers["accept-language"];
119
+ if (acceptLang) {
120
+ // Parse the Accept-Language header, get the first language
121
+ let preferred = acceptLang.split(",")[0].trim();
122
+ if (preferred) {
123
+ lang = preferred.split("-")[0];
124
+ langRegion = preferred;
125
+ // --- BEGIN OPTIONAL REDIRECT ---
126
+ // Uncomment to enable automatic redirect to language-specific path
127
+ // let newPath = `/${preferred}${pathname.startsWith('/') ? '' : '/'}${pathname.replace(/^\//, '')}`;
128
+ // res.writeHead(302, { Location: newPath });
129
+ // return res.end();
130
+ // --- END OPTIONAL REDIRECT ---
131
+ }
132
+ }
133
+ data.$filter.query.pathname = pathname;
134
+ }
135
+
136
+ let file;
137
+ if (
138
+ pathname.startsWith("/dist") ||
139
+ pathname.startsWith("/admin") ||
140
+ [
141
+ "/403.html",
142
+ "/404.html",
143
+ "/offline.html",
144
+ "/manifest.webmanifest",
145
+ "/service-worker.js"
146
+ ].includes(pathname)
147
+ ) {
148
+ file = await getDefaultFile(pathname);
149
+ } else {
150
+ file = await crud.send(data);
151
+ }
152
+
153
+ // --- Wildcard fallback ---
154
+ if (!file || !file.object || !file.object[0]) {
155
+ pathname = urlObject.pathname;
156
+ let lastIndex = pathname.lastIndexOf("/");
157
+ let wildcardPath = pathname.substring(0, lastIndex + 1);
158
+ let wildcard = pathname.substring(lastIndex + 1);
159
+
160
+ if (wildcard.includes(".")) {
161
+ let fileLastIndex = wildcard.lastIndexOf(".");
162
+ let fileExtension = wildcard.substring(fileLastIndex);
163
+ wildcard = wildcardPath + "*" + fileExtension; // Create wildcard for file name
164
+ } else {
165
+ wildcard = wildcardPath + "*/index.html"; // Append '*' if it's just a path or folder
166
+ }
167
+
168
+ data.$filter.query.pathname = wildcard;
169
+ file = await crud.send(data);
170
+ }
171
+
172
+ if (!file || !file.object || !file.object[0]) {
173
+ let pageNotFound = await getDefaultFile("/404.html");
174
+ return sendResponse(pageNotFound.object[0].src, 404, {
175
+ "Content-Type": "text/html"
176
+ });
177
+ }
178
+
179
+ file = file.object[0];
180
+ if (!file["public"] || file["public"] === "false") {
181
+ let pageForbidden = await getDefaultFile("/403.html");
182
+ return sendResponse(pageForbidden.object[0].src, 403, {
183
+ "Content-Type": "text/html"
184
+ });
185
+ }
186
+
187
+ let src;
188
+ if (file["src"]) {
189
+ src = file["src"];
190
+ } else {
191
+ let fileSrc = await crud.send({
192
+ method: "object.read",
193
+ host: hostname,
194
+ array: file["array"],
195
+ object: {
196
+ _id: file._id
197
+ },
198
+ organization_id
199
+ });
200
+ src = fileSrc[file["name"]];
201
+ }
202
+
203
+ if (!src) {
204
+ let pageNotFound = await getDefaultFile("/404.html");
205
+ return sendResponse(pageNotFound.object[0].src, 404, {
206
+ "Content-Type": "text/html"
207
+ });
208
+ }
209
+
210
+ let modifiedOn = file.modified || file.created;
211
+ if (modifiedOn) {
212
+ modifiedOn = modifiedOn.on;
213
+ if (modifiedOn instanceof Date)
214
+ modifiedOn = modifiedOn.toISOString();
215
+ res.setHeader("Last-Modified", modifiedOn);
216
+ }
217
+
218
+ let contentType = file["content-type"] || "text/html";
219
+
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
+ }
256
+ }
257
+ // Raster images and Icons -> Buffer
258
+ // Added x-icon, vnd.microsoft.icon, and ico to process icon files properly
259
+ if (/^image\/(png|jpe?g|webp|bmp|gif|x-icon|vnd\.microsoft\.icon|ico)$/i.test(d.mime)) {
260
+ if (d.isBase64) return Buffer.from(d.data, "base64");
261
+ }
262
+ // If it's a text data URI (e.g., xml) and not base64, return decoded text
263
+ if (!d.isBase64) {
264
+ try {
265
+ return decodeURIComponent(d.data);
266
+ } catch (_) {
267
+ return d.data;
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ // Plain base64-only string: decode to Buffer only for binary content types
274
+ if (isBase64Only(src)) {
275
+ if (
276
+ contentType.startsWith("image/") ||
277
+ contentType.startsWith("font/") ||
278
+ contentType === "application/octet-stream"
279
+ ) {
280
+ return Buffer.from(src, "base64");
281
+ }
282
+ // textual content kept as-is
283
+ return src;
284
+ }
285
+
286
+ // HTML rendering
287
+ if (contentType === "text/html") {
288
+ return await this.render.HTML(
289
+ file,
290
+ organization,
291
+ urlObject,
292
+ langRegion,
293
+ lang,
294
+ theme
295
+ );
296
+ }
297
+
298
+ // XML host replacement if src is string
299
+ if ((contentType === "text/xml" || contentType === "application/xml") && typeof src === "string") {
300
+ const protocol = "https://";
301
+ return src.replaceAll("{{$host}}", `${protocol}${hostname}`);
302
+ }
303
+
304
+ // Otherwise return as-is (string, Buffer, object)
305
+ return src;
306
+ }
307
+
308
+ try {
309
+ src = await normalizeSrc.call(this, src, contentType, pathname);
310
+ } catch (err) {
311
+ console.error("Error processing file src:", err && err.message);
312
+ let pageNotFound = await getDefaultFile("/404.html");
313
+ return sendResponse(pageNotFound.object[0].src, 404, {
314
+ "Content-Type": "text/html"
315
+ });
316
+ }
317
+
318
+ sendResponse(src, 200, { "Content-Type": contentType });
319
+ this.sitemap.check(file, hostname);
320
+
321
+ function sendResponse(src, statusCode, headers) {
322
+ try {
323
+ let cacheControl;
324
+ if (statusCode >= 400) {
325
+ cacheControl = "no-cache, no-store, must-revalidate";
326
+ } else if (
327
+ file &&
328
+ file["cache-control"] !== undefined &&
329
+ file["cache-control"] !== null
330
+ ) {
331
+ const val = file["cache-control"];
332
+ if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
333
+ // If it's numeric (number or numeric string) treat it as max-age.
334
+ cacheControl = `public, max-age=${String(
335
+ val
336
+ ).trim()};`;
337
+ } else {
338
+ // use the value verbatim (allow full Cache-Control strings like "no-cache" or "public, max-age=600")
339
+ cacheControl = val;
340
+ }
341
+ } else {
342
+ cacheControl = organization["cache-control"] || "public, max-age=3600";
343
+ }
344
+
345
+ // Always override/set Cache-Control header so the response aligns with the file metadata/defaults
346
+ headers["Cache-Control"] = cacheControl;
347
+ // Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
348
+ // After the browser sees this in a response it may include
349
+ // the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
350
+ headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
351
+ // optional: tell browser how long to remember this preference (seconds)
352
+ headers["Accept-CH-Lifetime"] = "86400";
353
+ // ensure caches/proxies vary responses by this hint
354
+ headers["Vary"] = headers["Vary"]
355
+ ? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
356
+ : "Sec-CH-Prefers-Color-Scheme";
357
+
358
+ if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
359
+ // Ensure binary data is sent as-is
360
+ } else if (typeof src === "object") {
361
+ src = JSON.stringify(src);
362
+ }
363
+
364
+ if (organization_id)
365
+ crud.wsManager.emit("setBandwidth", {
366
+ type: "out",
367
+ data: src,
368
+ organization_id
369
+ });
370
+ res.writeHead(statusCode, headers);
371
+ return res.end(src);
372
+ } catch (error) {
373
+ console.log(error);
374
+ }
375
+ }
376
+
377
+ async function getDefaultFile(fileName) {
378
+ data.$filter.query.pathname = fileName;
379
+ // data.$filter.query.$or[0] = { pathname: fileName }
380
+ let defaultFile;
381
+ if (fileName !== "/hostNotFound.html")
382
+ defaultFile = await crud.send(data);
383
+
384
+ if (
385
+ defaultFile &&
386
+ defaultFile.object &&
387
+ defaultFile.object[0] &&
388
+ defaultFile.object[0].src
389
+ ) {
390
+ return defaultFile;
391
+ } else {
392
+ data.$filter.query.host.$in = ["*"];
393
+ data.organization_id = process.env.organization_id;
394
+
395
+ if (fileName.startsWith("/admin"))
396
+ data.$filter.query.pathname =
397
+ "/superadmin" + fileName.replace("/admin", "");
398
+
399
+ defaultFile = await crud.send(data);
400
+
401
+ if (fileName !== "/hostNotFound.html") {
402
+ crud.wsManager.emit("setBandwidth", {
403
+ type: "out",
404
+ data,
405
+ organization_id
406
+ });
407
+
408
+ crud.wsManager.emit("setBandwidth", {
409
+ type: "in",
410
+ data: defaultFile,
411
+ organization_id
412
+ });
413
+ }
414
+
415
+ if (
416
+ defaultFile &&
417
+ defaultFile.object &&
418
+ defaultFile.object[0] &&
419
+ defaultFile.object[0].src
420
+ ) {
421
+ if (fileName.startsWith("/admin")) {
422
+ data.object[0].directory = "admin";
423
+ data.object[0].path =
424
+ "/admin" +
425
+ data.object[0].path.replace("/superadmin", "");
426
+ data.object[0].pathname = fileName;
427
+ }
428
+
429
+ crud.send({
430
+ method: "object.create",
431
+ host: hostname,
432
+ array: "files",
433
+ object: defaultFile.object[0],
434
+ organization_id
435
+ });
436
+
437
+ return defaultFile;
438
+ } else {
439
+ switch (fileName) {
440
+ case "/403.html":
441
+ defaultFile.object = [
442
+ {
443
+ src: `${pathname} access not allowed for ${organization_id}`
444
+ }
445
+ ];
446
+ break;
447
+ case "/404.html":
448
+ defaultFile.object = [
449
+ {
450
+ src: `${pathname} could not be found for ${organization_id}`
451
+ }
452
+ ];
453
+ break;
454
+ case "/balanceFalse.html":
455
+ defaultFile.object = [
456
+ {
457
+ src: "This organizations account balance has fallen bellow 0: "
458
+ }
459
+ ];
460
+ break;
461
+ case "/hostNotFound.html":
462
+ defaultFile.object = [
463
+ {
464
+ src:
465
+ "An organization could not be found using the host: " +
466
+ hostname +
467
+ " in platformDB: " +
468
+ process.env.organization_id
469
+ }
470
+ ];
471
+ break;
472
+ }
473
+ return defaultFile;
474
+ }
475
+ }
476
+ }
477
+ } catch (error) {
478
+ res.writeHead(400, { "Content-Type": "text/plain" });
479
+ res.end("Invalid host format");
480
+ }
481
+ }
481
482
  }
482
483
 
483
- module.exports = CoCreateFileSystem;
484
+ module.exports = CoCreateFileSystem;