@cocreate/file-server 1.18.9 → 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,17 @@
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
+
8
+ ## [1.18.10](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.9...v1.18.10) (2025-11-17)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * enhance font and data URI handling in file processing ([25b7e8c](https://github.com/CoCreate-app/CoCreate-file-server/commit/25b7e8c1976c12ea99f1806025c248e0fa865ef0))
14
+
1
15
  ## [1.18.9](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.8...v1.18.9) (2025-10-11)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/file-server",
3
- "version": "1.18.9",
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,429 +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
- // Remove redundant handling for `src.src` in font file processing
221
- if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
222
- try {
223
- if (typeof src === "string") {
224
- if (src.startsWith("data:font/")) {
225
- src = src.substring(src.indexOf(",") + 1);
226
- }
227
- if (/^([A-Za-z0-9+/]+={0,2})$/.test(src)) {
228
- // Decode base64-encoded font data
229
- src = Buffer.from(src, "base64");
230
- } else {
231
- throw new Error("Font data is not valid base64");
232
- }
233
- } else {
234
- throw new Error("Font data is not a valid base64 string");
235
- }
236
- } catch (err) {
237
- console.error("Error processing font file:", {
238
- message: err.message,
239
- contentType,
240
- srcType: typeof src
241
- });
242
- let pageNotFound = await getDefaultFile("/404.html");
243
- return sendResponse(pageNotFound.object[0].src, 404, {
244
- "Content-Type": "text/html"
245
- });
246
- }
247
- } else if (
248
- /^data:image\/[a-zA-Z0-9+.-]+;base64,([A-Za-z0-9+/]+={0,2})$/.test(
249
- src
250
- )
251
- ) {
252
- src = src.replace(/^data:image\/(png|jpeg|jpg);base64,/, "");
253
- src = Buffer.from(src, "base64");
254
- } else if (/^([A-Za-z0-9+/]+={0,2})$/.test(src)) {
255
- src = Buffer.from(src, "base64");
256
- } else if (contentType === "text/html") {
257
- try {
258
- let data = {};
259
-
260
- // pass detected theme (may be undefined) into server-side renderer
261
- src = await this.render.HTML(
262
- file,
263
- organization,
264
- urlObject,
265
- langRegion,
266
- lang,
267
- theme
268
- );
269
- } catch (err) {
270
- console.warn("server-side-render: " + err.message);
271
- }
272
- } else if (
273
- contentType === "text/xml" ||
274
- contentType === "application/xml"
275
- ) {
276
- const protocol = "https://";
277
- src = src.replaceAll("{{$host}}", `${protocol}${hostname}`);
278
- } else if (contentType !== "text/javascript" || contentType === "text/css") {
279
-
280
- console.warn(`Unknown content type: ${contentType}`);
281
- }
282
-
283
- sendResponse(src, 200, { "Content-Type": contentType });
284
- this.sitemap.check(file, hostname);
285
-
286
- function sendResponse(src, statusCode, headers) {
287
- try {
288
- let cacheControl;
289
- if (statusCode >= 400) {
290
- cacheControl = "no-cache, no-store, must-revalidate";
291
- } else if (
292
- file &&
293
- file["cache-control"] !== undefined &&
294
- file["cache-control"] !== null
295
- ) {
296
- const val = file["cache-control"];
297
- if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
298
- // If it's numeric (number or numeric string) treat it as max-age.
299
- cacheControl = `public, max-age=${String(
300
- val
301
- ).trim()};`;
302
- } else {
303
- // use the value verbatim (allow full Cache-Control strings like "no-cache" or "public, max-age=600")
304
- cacheControl = val;
305
- }
306
- } else {
307
- cacheControl = organization["cache-control"] || "public, max-age=3600";
308
- }
309
-
310
- // Always override/set Cache-Control header so the response aligns with the file metadata/defaults
311
- headers["Cache-Control"] = cacheControl;
312
- // Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
313
- // After the browser sees this in a response it may include
314
- // the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
315
- headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
316
- // optional: tell browser how long to remember this preference (seconds)
317
- headers["Accept-CH-Lifetime"] = "86400";
318
- // ensure caches/proxies vary responses by this hint
319
- headers["Vary"] = headers["Vary"]
320
- ? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
321
- : "Sec-CH-Prefers-Color-Scheme";
322
-
323
- if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
324
- // Ensure binary data is sent as-is
325
- } else if (typeof src === "object") {
326
- src = JSON.stringify(src);
327
- }
328
-
329
- if (organization_id)
330
- crud.wsManager.emit("setBandwidth", {
331
- type: "out",
332
- data: src,
333
- organization_id
334
- });
335
- res.writeHead(statusCode, headers);
336
- return res.end(src);
337
- } catch (error) {
338
- console.log(error);
339
- }
340
- }
341
-
342
- async function getDefaultFile(fileName) {
343
- data.$filter.query.pathname = fileName;
344
- // data.$filter.query.$or[0] = { pathname: fileName }
345
- let defaultFile;
346
- if (fileName !== "/hostNotFound.html")
347
- defaultFile = await crud.send(data);
348
-
349
- if (
350
- defaultFile &&
351
- defaultFile.object &&
352
- defaultFile.object[0] &&
353
- defaultFile.object[0].src
354
- ) {
355
- return defaultFile;
356
- } else {
357
- data.$filter.query.host.$in = ["*"];
358
- data.organization_id = process.env.organization_id;
359
-
360
- if (fileName.startsWith("/admin"))
361
- data.$filter.query.pathname =
362
- "/superadmin" + fileName.replace("/admin", "");
363
-
364
- defaultFile = await crud.send(data);
365
-
366
- if (fileName !== "/hostNotFound.html") {
367
- crud.wsManager.emit("setBandwidth", {
368
- type: "out",
369
- data,
370
- organization_id
371
- });
372
-
373
- crud.wsManager.emit("setBandwidth", {
374
- type: "in",
375
- data: defaultFile,
376
- organization_id
377
- });
378
- }
379
-
380
- if (
381
- defaultFile &&
382
- defaultFile.object &&
383
- defaultFile.object[0] &&
384
- defaultFile.object[0].src
385
- ) {
386
- if (fileName.startsWith("/admin")) {
387
- data.object[0].directory = "admin";
388
- data.object[0].path =
389
- "/admin" +
390
- data.object[0].path.replace("/superadmin", "");
391
- data.object[0].pathname = fileName;
392
- }
393
-
394
- crud.send({
395
- method: "object.create",
396
- host: hostname,
397
- array: "files",
398
- object: defaultFile.object[0],
399
- organization_id
400
- });
401
-
402
- return defaultFile;
403
- } else {
404
- switch (fileName) {
405
- case "/403.html":
406
- defaultFile.object = [
407
- {
408
- src: `${pathname} access not allowed for ${organization_id}`
409
- }
410
- ];
411
- break;
412
- case "/404.html":
413
- defaultFile.object = [
414
- {
415
- src: `${pathname} could not be found for ${organization_id}`
416
- }
417
- ];
418
- break;
419
- case "/balanceFalse.html":
420
- defaultFile.object = [
421
- {
422
- src: "This organizations account balance has fallen bellow 0: "
423
- }
424
- ];
425
- break;
426
- case "/hostNotFound.html":
427
- defaultFile.object = [
428
- {
429
- src:
430
- "An organization could not be found using the host: " +
431
- hostname +
432
- " in platformDB: " +
433
- process.env.organization_id
434
- }
435
- ];
436
- break;
437
- }
438
- return defaultFile;
439
- }
440
- }
441
- }
442
- } catch (error) {
443
- res.writeHead(400, { "Content-Type": "text/plain" });
444
- res.end("Invalid host format");
445
- }
446
- }
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
+ }
447
482
  }
448
483
 
449
- module.exports = CoCreateFileSystem;
484
+ module.exports = CoCreateFileSystem;