@cocreate/file-server 1.19.0 → 1.21.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.
@@ -1,8 +1,10 @@
1
1
  name: Automated Workflow
2
+
2
3
  on:
3
4
  push:
4
5
  branches:
5
6
  - master
7
+
6
8
  jobs:
7
9
  about:
8
10
  runs-on: ubuntu-latest
@@ -18,52 +20,37 @@ jobs:
18
20
  with:
19
21
  direction: overwrite-github
20
22
  githubToken: "${{ secrets.GITHUB }}"
23
+
21
24
  release:
22
25
  runs-on: ubuntu-latest
23
26
  steps:
24
27
  - name: Checkout
25
28
  uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0 # Required so semantic-release can trace git tags/history
31
+
26
32
  - name: Setup Node.js
27
33
  uses: actions/setup-node@v4
28
34
  with:
29
- node-version: 22 # Required for the latest semantic-release plugins
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v4 # Update to v4 for better Node 20+ support
32
- id: semantic
33
- with:
34
- extra_plugins: |
35
- @semantic-release/changelog
35
+ node-version: 22
36
+
37
+ - name: Install Semantic Release & Plugins
38
+ # Installs semantic-release and its plugins on the runner
39
+ run: |
40
+ npm install -g semantic-release \
41
+ @semantic-release/changelog \
42
+ @semantic-release/npm \
43
+ @semantic-release/github \
36
44
  @semantic-release/git
37
- @semantic-release/github
45
+
46
+ - name: Run Semantic Release (Native)
47
+ id: semantic
48
+ # This will automatically pick up your export default config file in the repository root
49
+ run: npx semantic-release
38
50
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Use the built-in token if possible
51
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
40
52
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
53
+
41
54
  outputs:
42
55
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
43
- new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
44
- upload:
45
- runs-on: ubuntu-latest
46
- needs: release
47
- if: needs.release.outputs.new_release_published == 'true'
48
- env:
49
- VERSION: "${{ needs.release.outputs.new_release_version }}"
50
- steps:
51
- - name: Checkout
52
- uses: actions/checkout@v3
53
- - name: Setup Node.js
54
- uses: actions/setup-node@v3
55
- with:
56
- node-version: 16
57
- - name: Set npm registry auth
58
- run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
59
- - name: Install dependencies
60
- run: yarn install
61
-
62
- - name: Set Environment Variables
63
- run: |
64
- echo "organization_id=${{ secrets.COCREATE_ORGANIZATION_ID }}" >> $GITHUB_ENV
65
- echo "key=${{ secrets.COCREATE_KEY }}" >> $GITHUB_ENV
66
- echo "host=${{ secrets.COCREATE_HOST }}" >> $GITHUB_ENV
67
- - name: CoCreate Upload
68
- run: coc upload
69
-
56
+ new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ # [1.21.0](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.20.0...v1.21.0) (2026-07-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * update automated workflow and release configuration for improved semantic release handling ([46e48c9](https://github.com/CoCreate-app/CoCreate-file-server/commit/46e48c906291f7939b69ff4064f4dd5413649d87))
7
+
8
+ # [1.20.0](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.19.1...v1.20.0) (2026-07-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * add redirects for missing and forbidden files to /404.html and /403.html ([906e874](https://github.com/CoCreate-app/CoCreate-file-server/commit/906e874fbf0790f3e08a119b5215f0e6e53c023b))
14
+ * improve HTML request handling and response for 404 and 403 errors ([b78e21d](https://github.com/CoCreate-app/CoCreate-file-server/commit/b78e21de0887f37f3cc0156c339d6203c241a697))
15
+ * removed post install ([95cc099](https://github.com/CoCreate-app/CoCreate-file-server/commit/95cc099f2e6d79755ded981543e7e1ea9b78a08d))
16
+ * semantic version handling. Reorganize .gitignore for improved clarity and structure ([cabc86c](https://github.com/CoCreate-app/CoCreate-file-server/commit/cabc86c16a7a9ad6855e4bd529a4df060a6e11a8))
17
+ * streamline error handling and response for missing files and improve status code management ([3f9b253](https://github.com/CoCreate-app/CoCreate-file-server/commit/3f9b25329e31d9736d635e22b5ff99244f698f53))
18
+ * update module export to ES6 syntax in release.config.js ([ecee757](https://github.com/CoCreate-app/CoCreate-file-server/commit/ecee757539946a0304d6c8551b74969a8cf6c3f3))
19
+ * update package.json and refactor index.js for module compatibility ([b140de1](https://github.com/CoCreate-app/CoCreate-file-server/commit/b140de1b20b727c37333be5cf8d36543c892daca))
20
+
21
+
22
+ ### Features
23
+
24
+ * enforce trailing slash for directory paths in URL handling ([aab2d0e](https://github.com/CoCreate-app/CoCreate-file-server/commit/aab2d0eaf095efe302680f1ba5a5a4eeba1b0649))
25
+ * update package.json and refactor index.js to use ES modules ([fd121ff](https://github.com/CoCreate-app/CoCreate-file-server/commit/fd121ffa438d174e3a11b2bec8d001718c193e08))
26
+
27
+ ## [1.19.1](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.19.0...v1.19.1) (2026-04-15)
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * video and audio handling ([e7b67c6](https://github.com/CoCreate-app/CoCreate-file-server/commit/e7b67c66930d309b1deeefda5720134106de2b91))
33
+
1
34
  # [1.19.0](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.18.10...v1.19.0) (2026-04-08)
2
35
 
3
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/file-server",
3
- "version": "1.19.0",
3
+ "version": "1.21.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",
@@ -18,8 +18,7 @@
18
18
  "scripts": {
19
19
  "start": "npx webpack --config webpack.config.js",
20
20
  "build": "npx webpack --mode=production --config webpack.config.js",
21
- "dev": "npx webpack --config webpack.config.js --watch",
22
- "postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
21
+ "dev": "npx webpack --config webpack.config.js --watch"
23
22
  },
24
23
  "repository": {
25
24
  "type": "git",
@@ -35,5 +34,10 @@
35
34
  "type": "GitHub Sponsors ❤",
36
35
  "url": "https://github.com/sponsors/CoCreate-app"
37
36
  },
38
- "main": "./src/index.js"
37
+ "type": "module",
38
+ "main": "./src/index.js",
39
+ "dependencies": {
40
+ "@cocreate/server-side-render": "^1.12.8",
41
+ "@cocreate/sitemap": "^1.4.2"
42
+ }
39
43
  }
package/release.config.js CHANGED
@@ -1,4 +1,4 @@
1
- module.exports = {
1
+ export default {
2
2
  dryRun: false,
3
3
  branches: ["master"],
4
4
  plugins: [
@@ -10,12 +10,20 @@ module.exports = {
10
10
  changelogFile: "CHANGELOG.md",
11
11
  },
12
12
  ],
13
- "@semantic-release/npm",
14
- "@semantic-release/github",
13
+ "@semantic-release/npm",
15
14
  [
16
- "@semantic-release/git",
15
+ "@semantic-release/github",
17
16
  {
17
+ successComment: false,
18
+ failTitle: false,
19
+ },
20
+ ],
21
+ [
22
+ "@semantic-release/git",
23
+ {
24
+ // Only stage and commit the changelog and package.json
18
25
  assets: ["CHANGELOG.md", "package.json"],
26
+ message: "chore(release): ${nextRelease.version} [skip ci]",
19
27
  },
20
28
  ],
21
29
  ],
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright (C) 2023 CoCreate and Contributors.
3
3
  *
4
4
  * This program is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU Affero General Public License as published
5
+ * Wu under the terms of the GNU Affero General Public License as published
6
6
  * by the Free Software Foundation, either version 3 of the License, or
7
7
  * (at your option) any later version.
8
8
  *
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * You should have received a copy of the GNU Affero General Public License
15
15
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
16
17
  ********************************************************************************/
17
18
 
18
19
  // Commercial Licensing Information:
@@ -20,465 +21,593 @@
20
21
  // you must obtain a commercial license from CoCreate LLC.
21
22
  // For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
22
23
 
23
- class CoCreateFileSystem {
24
- constructor(render, sitemap) {
25
- this.render = render;
26
- this.sitemap = sitemap;
27
- }
24
+ import * as render from '@cocreate/server-side-render';
25
+ import * as sitemap from '@cocreate/sitemap';
28
26
 
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 ---
27
+ // Module-level reference injected on-demand during boot to prevent ESM circular loops
28
+ let server = null;
50
29
 
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
- });
30
+ /**
31
+ * Initializes the File Server module by injecting the parent orchestrator context.
32
+ * Bypasses ESM static compile cycles entirely.
33
+ * @param {Object} Server - CoCreateServer context instance.
34
+ */
35
+ export function init(Server) {
36
+ server = Server;
37
+ return FileServer
38
+ }
39
+
40
+ /**
41
+ * Main stateless entry point for the CoCreate File System request handler.
42
+ * Now cleanly self-contained, handling its own urlObject parsing and organization lookups.
43
+ * @param {Object} req - Native Node HTTP request object
44
+ * @param {Object} res - Native Node HTTP response object
45
+ */
46
+ export async function send(req, res) {
47
+ try {
48
+ const activeCrud = server ? server.crud : null;
49
+
50
+ // Parse the URL structure internally from the incoming request
51
+ const urlObject = new URL(`http://${req.headers.host || 'localhost'}${req.url}`);
52
+ const hostname = urlObject.hostname;
53
+ let pathname = urlObject.pathname;
54
+
55
+ // Resolve Organization dynamically from the injected CRUD instance
56
+ let organization = null;
57
+ if (activeCrud) {
58
+ try {
59
+ organization = await activeCrud.getOrganization({ host: hostname });
60
+ } catch (err) {
61
+ console.error("[@cocreate/file-server] Failed to resolve organization details internally:", err);
69
62
  }
63
+ }
64
+
65
+ // 1. Check Organization active status
66
+ if (!organization || organization.error) {
67
+ const hostNotFound = await getDefaultFile({ fileName: "/hostNotFound.html", hostname, organizationId: null, crud: activeCrud });
68
+ sendResponse({
69
+ res,
70
+ src: hostNotFound.object[0].src,
71
+ statusCode: 404,
72
+ headers: { "Content-Type": hostNotFound.object[0]["content-type"] || "text/html" },
73
+ file: hostNotFound.object[0],
74
+ organizationId: null,
75
+ crud: activeCrud
76
+ });
77
+ return;
78
+ }
70
79
 
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",
80
+ const organizationId = organization._id;
81
+ res.setHeader("organization", organizationId);
82
+ res.setHeader("storage", !!organization.storage);
83
+ res.setHeader("Access-Control-Allow-Origin", "*");
84
+ res.setHeader("Access-Control-Allow-Methods", "");
85
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
86
+
87
+ const wsManager = server ? server.wsManager : null;
88
+ const active = wsManager ? wsManager.organizations.get(organizationId) : true;
89
+ if (active === false) {
90
+ const balanceFalse = await getDefaultFile({ fileName: "/balanceFalse.html", hostname, organizationId, crud: activeCrud });
91
+ sendResponse({
92
+ res,
93
+ src: balanceFalse.object[0].src,
94
+ statusCode: 403,
95
+ headers: {
96
+ "Content-Type": balanceFalse.object[0]["content-type"] || "text/html",
88
97
  "Account-Balance": "false",
89
98
  storage: organization.storage
90
- });
91
- }
99
+ },
100
+ file: balanceFalse.object[0],
101
+ organizationId,
102
+ crud: activeCrud
103
+ });
104
+ return;
105
+ }
106
+
107
+ // 2. Trailing Slash Redirect
108
+ if (handleTrailingSlash(pathname, urlObject, res)) {
109
+ return; // Redirect occurred, execution halted
110
+ }
92
111
 
93
- let parameters = urlObject.searchParams;
94
- if (parameters.size) {
95
- console.log("parameters", parameters);
112
+ // 3. Theme Detection
113
+ const theme = detectTheme(req, res);
114
+
115
+ // 4. Resolve exact file resource and status code
116
+ const { file, src, statusCode, resolvedPathname } = await resolveFile({
117
+ pathname,
118
+ hostname,
119
+ organizationId,
120
+ crud: activeCrud
121
+ });
122
+
123
+ // 5. Language Localization Matching
124
+ const { lang, langRegion } = parseLanguage(resolvedPathname, req?.headers);
125
+
126
+ let contentType = file?.["content-type"] || "text/html";
127
+
128
+ // 6. Output Normalization and Rendering
129
+ const finalSrc = await normalizeSrc({
130
+ src,
131
+ contentType,
132
+ pathname: resolvedPathname,
133
+ file,
134
+ organization,
135
+ urlObject,
136
+ langRegion,
137
+ lang,
138
+ theme,
139
+ crud: activeCrud
140
+ });
141
+
142
+ let modifiedOn = file?.modified || file?.created;
143
+ if (modifiedOn) {
144
+ modifiedOn = modifiedOn.on;
145
+ if (modifiedOn instanceof Date) {
146
+ modifiedOn = modifiedOn.toISOString();
96
147
  }
148
+ res.setHeader("Last-Modified", modifiedOn);
149
+ }
97
150
 
98
- let pathname = urlObject.pathname;
151
+ // 7. Send final response
152
+ sendResponse({
153
+ res,
154
+ src: finalSrc,
155
+ statusCode,
156
+ headers: { "Content-Type": contentType },
157
+ file,
158
+ organizationId: organizationId,
159
+ crud: activeCrud,
160
+ organizationCacheControl: organization["cache-control"]
161
+ });
162
+
163
+ // 8. Async Sitemap Check
164
+ if (file) {
165
+ await sitemap.check({ file, host: hostname, crud: activeCrud });
166
+ }
99
167
 
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
- }
168
+ } catch (error) {
169
+ console.error("Critical error in FileSystem handler:", error);
170
+ res.writeHead(400, { "Content-Type": "text/plain" });
171
+ res.end("Invalid host format");
172
+ }
173
+ }
106
174
 
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
- }
175
+ // ==========================================
176
+ // Isolated Helper Functions
177
+ // ==========================================
178
+
179
+ /**
180
+ * Normalizes slash-ending paths, resolves files via wildcard mappings,
181
+ * handles 404/403 fallbacks, and fetches asset sources.
182
+ */
183
+ export async function resolveFile({ pathname, hostname, organizationId, crud }) {
184
+ const activeCrud = crud || (server ? server.crud : null);
185
+ let statusCode = 200;
186
+ let file;
187
+
188
+ if (pathname.endsWith("/")) {
189
+ pathname += "index.html";
190
+ }
135
191
 
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
- }
192
+ const queryData = {
193
+ method: "object.read",
194
+ host: hostname,
195
+ array: "files",
196
+ organization_id: organizationId,
197
+ $filter: {
198
+ query: {
199
+ host: { $in: [hostname, "*"] },
200
+ pathname
201
+ },
202
+ limit: 1
203
+ }
204
+ };
205
+
206
+ // 1. Fetch File
207
+ if (
208
+ pathname.startsWith("/dist") ||
209
+ pathname.startsWith("/admin") ||
210
+ [
211
+ "/403.html",
212
+ "/404.html",
213
+ "/offline.html",
214
+ "/manifest.webmanifest",
215
+ "/service-worker.js"
216
+ ].includes(pathname)
217
+ ) {
218
+ const defaultResult = await getDefaultFile({ fileName: pathname, hostname, organizationId, crud: activeCrud });
219
+ file = defaultResult?.object?.[0];
220
+ } else {
221
+ const result = await activeCrud.send(queryData);
222
+ file = result?.object?.[0];
223
+ }
152
224
 
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
- }
225
+ // 2. Wildcard fallback
226
+ if (!file) {
227
+ const wildcardPath = getWildcardPath(pathname);
228
+ queryData.$filter.query.pathname = wildcardPath;
229
+ const result = await activeCrud.send(queryData);
230
+ file = result?.object?.[0];
231
+ }
167
232
 
168
- data.$filter.query.pathname = wildcard;
169
- file = await crud.send(data);
170
- }
233
+ // 3. Fallback to 404 if file completely missing
234
+ if (!file) {
235
+ statusCode = 404;
236
+ const pageNotFound = await getDefaultFile({ fileName: "/404.html", hostname, organizationId, crud: activeCrud });
237
+ file = pageNotFound?.object?.[0];
238
+ }
171
239
 
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
- }
240
+ // 4. Fallback to 403 if not public
241
+ if (file && (!file["public"] || file["public"] === "false")) {
242
+ statusCode = 403;
243
+ const pageForbidden = await getDefaultFile({ fileName: "/403.html", hostname, organizationId, crud: activeCrud });
244
+ file = pageForbidden?.object?.[0];
245
+ }
178
246
 
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
- }
247
+ // 5. Fetch file source markup
248
+ let src = file?.src || "";
249
+ if (file && !file["src"]) {
250
+ try {
251
+ const fileSrc = await activeCrud.send({
252
+ method: "object.read",
253
+ host: hostname,
254
+ array: file["array"],
255
+ object: { _id: file._id },
256
+ organization_id: organizationId
257
+ });
258
+ src = fileSrc?.[file["name"]] || "";
259
+ } catch (err) {
260
+ console.error("Error fetching file src:", err);
261
+ }
262
+ }
186
263
 
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
- }
264
+ // 6. Ultimate 404 payload fallback if source resolves empty
265
+ if (!src && file) {
266
+ statusCode = 404;
267
+ const pageNotFound = await getDefaultFile({ fileName: "/404.html", hostname, organizationId, crud: activeCrud });
268
+ file = pageNotFound?.object?.[0];
269
+ src = file?.src || "";
270
+ }
202
271
 
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
- }
272
+ // Bind source markup and calculated status directly onto the file document for downstream consumers (like the SSR engine)
273
+ if (file) {
274
+ file.src = src;
275
+ file.status = statusCode;
276
+ }
209
277
 
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
- }
278
+ return { file, src, statusCode, resolvedPathname: pathname };
279
+ }
217
280
 
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
- }
281
+ /**
282
+ * Specifically resolves, tracks, and mirrors system fallbacks from global scopes.
283
+ */
284
+ export async function getDefaultFile({ fileName, hostname, organizationId, crud }) {
285
+ const activeCrud = crud || (server ? server.crud : null);
286
+ const queryData = {
287
+ method: "object.read",
288
+ host: hostname,
289
+ array: "files",
290
+ organization_id: organizationId,
291
+ $filter: {
292
+ query: {
293
+ host: { $in: [hostname, "*"] },
294
+ pathname: fileName
295
+ },
296
+ limit: 1
297
+ }
298
+ };
242
299
 
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
- }
300
+ let defaultFile;
301
+ if (fileName !== "/hostNotFound.html" && organizationId) {
302
+ defaultFile = await activeCrud.send(queryData);
303
+ }
272
304
 
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
- }
305
+ if (defaultFile?.object?.[0]?.src) {
306
+ return defaultFile;
307
+ }
285
308
 
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
- }
309
+ // Search global scope platform database
310
+ queryData.$filter.query.host.$in = ["*"];
311
+ queryData.organization_id = process.env.organization_id;
297
312
 
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
- }
313
+ if (fileName.startsWith("/admin")) {
314
+ queryData.$filter.query.pathname = "/superadmin" + fileName.replace("/admin", "");
315
+ }
303
316
 
304
- // Otherwise return as-is (string, Buffer, object)
305
- return src;
306
- }
317
+ defaultFile = await activeCrud.send(queryData);
318
+
319
+ const wsManager = server ? server.wsManager : null;
320
+ if (fileName !== "/hostNotFound.html" && organizationId && wsManager) {
321
+ wsManager.emit("setBandwidth", {
322
+ type: "out",
323
+ data: queryData,
324
+ organization_id: organizationId
325
+ });
326
+
327
+ wsManager.emit("setBandwidth", {
328
+ type: "in",
329
+ data: defaultFile,
330
+ organization_id: organizationId
331
+ });
332
+ }
307
333
 
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);
334
+ if (defaultFile?.object?.[0]?.src) {
335
+ if (fileName.startsWith("/admin")) {
336
+ defaultFile.object[0].directory = "admin";
337
+ defaultFile.object[0].path = "/admin" + defaultFile.object[0].path.replace("/superadmin", "");
338
+ defaultFile.object[0].pathname = fileName;
339
+ }
340
+
341
+ if (organizationId) {
342
+ // Save local cloned copy inside this organization's database
343
+ await activeCrud.send({
344
+ method: "object.create",
345
+ host: hostname,
346
+ array: "files",
347
+ object: defaultFile.object[0],
348
+ organization_id: organizationId
349
+ });
350
+ }
351
+
352
+ return defaultFile;
353
+ }
354
+
355
+ // Hard fallback definitions in case DB collections are empty
356
+ const structuralFallback = { object: [{ src: "" }] };
357
+ switch (fileName) {
358
+ case "/403.html":
359
+ structuralFallback.object[0].src = `Access not allowed for ${organizationId}`;
360
+ break;
361
+ case "/404.html":
362
+ structuralFallback.object[0].src = `File could not be found for ${organizationId}`;
363
+ break;
364
+ case "/balanceFalse.html":
365
+ structuralFallback.object[0].src = "This organization's account balance has fallen below 0: ";
366
+ break;
367
+ case "/hostNotFound.html":
368
+ structuralFallback.object[0].src = `An organization could not be found using the host: ${hostname} in platformDB: ${process.env.organization_id}`;
369
+ break;
370
+ }
371
+ return structuralFallback;
372
+ }
373
+
374
+ /**
375
+ * Calculates a directory wildcard path pattern representation.
376
+ */
377
+ export function getWildcardPath(pathname) {
378
+ const lastIndex = pathname.lastIndexOf("/");
379
+ const wildcardPath = pathname.substring(0, lastIndex + 1);
380
+ const wildcardName = pathname.substring(lastIndex + 1);
381
+
382
+ if (wildcardName.includes(".")) {
383
+ const fileLastIndex = wildcardName.lastIndexOf(".");
384
+ const fileExtension = wildcardName.substring(fileLastIndex);
385
+ return wildcardPath + "*" + fileExtension;
386
+ } else {
387
+ return wildcardPath + "*/index.html";
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Detects color scheme from HTTP headers and updates client indicators.
393
+ */
394
+ export function detectTheme(req, res) {
395
+ let theme;
396
+ const headerValue = req?.headers && (
397
+ req.headers["sec-ch-prefers-color-scheme"] ||
398
+ req.headers["prefers-color-scheme"] ||
399
+ req.headers["x-prefers-color-scheme"] ||
400
+ req.headers["x-color-scheme"]
401
+ );
402
+ if (typeof headerValue === "string") {
403
+ const token = headerValue.split(",")[0].trim().replace(/^"|"$/g, "").toLowerCase();
404
+ if (token === "dark" || token === "light") {
405
+ theme = token;
406
+ }
407
+ }
408
+ if (theme && res) {
409
+ res.setHeader("X-Preferred-Color-Scheme", theme);
410
+ }
411
+ return theme;
412
+ }
413
+
414
+ /**
415
+ * Standardizes directory matching redirection configurations.
416
+ */
417
+ export function handleTrailingSlash(pathname, urlObject, res) {
418
+ if (!pathname.endsWith("/") && !pathname.startsWith("/.well-known/acme-challenge")) {
419
+ const lastSegment = pathname.split("/").slice(-1)[0];
420
+ if (!lastSegment.includes(".")) {
421
+ const search = urlObject.search || "";
422
+ res.writeHead(301, { Location: pathname + "/" + search });
423
+ res.end();
424
+ return true;
425
+ }
426
+ }
427
+ return false;
428
+ }
320
429
 
321
- function sendResponse(src, statusCode, headers) {
430
+ /**
431
+ * Extracts explicit or implicit preferred localization tokens from endpoints.
432
+ */
433
+ export function parseLanguage(pathname, headers) {
434
+ const bcp47Regex = /^\/([a-zA-Z]{2,3}(?:-[a-zA-Z0-9]{2,8})?)\//;
435
+ const langMatch = pathname.match(bcp47Regex);
436
+ let lang, langRegion;
437
+
438
+ if (langMatch) {
439
+ langRegion = langMatch[1];
440
+ lang = langRegion.split("-")[0];
441
+ const basePathname = pathname.replace("/" + langRegion, "");
442
+ return { lang, langRegion, basePathname };
443
+ }
444
+
445
+ const acceptLang = headers?.["accept-language"];
446
+ if (acceptLang) {
447
+ const preferred = acceptLang.split(",")[0].trim();
448
+ if (preferred) {
449
+ lang = preferred.split("-")[0];
450
+ langRegion = preferred;
451
+ }
452
+ }
453
+ return { lang, langRegion, basePathname: pathname };
454
+ }
455
+
456
+ /**
457
+ * Formats, cleans, parses, and executes rendering transformations based on content types.
458
+ */
459
+ export async function normalizeSrc({ src, contentType, pathname, file, organization, urlObject, langRegion, lang, theme, crud }) {
460
+ const activeCrud = crud || (server ? server.crud : null);
461
+ const isBase64Only = (s) =>
462
+ typeof s === "string" && /^[A-Za-z0-9+/]+={0,2}$/.test(s) && s.length % 4 === 0;
463
+
464
+ const parseDataUri = (s) => {
465
+ const m = /^data:([^;]+)(;base64)?,(.*)$/is.exec(s);
466
+ if (!m) return null;
467
+ return { mime: m[1].toLowerCase(), isBase64: !!m[2], data: m[3] };
468
+ };
469
+
470
+ // Fonts
471
+ if (contentType.startsWith("font/") || /\.(woff2?|ttf|otf)$/i.test(pathname)) {
472
+ if (Buffer.isBuffer(src)) return src;
473
+ if (typeof src !== "string") throw new Error("Invalid font src");
474
+ const d = parseDataUri(src);
475
+ if (d?.isBase64) return Buffer.from(d.data, "base64");
476
+ if (isBase64Only(src)) return Buffer.from(src, "base64");
477
+ throw new Error("Font data is not valid base64 or data URI");
478
+ }
479
+
480
+ // Data URIs
481
+ if (typeof src === "string") {
482
+ const d = parseDataUri(src);
483
+ if (d) {
484
+ if (d.mime === "image/svg+xml") {
485
+ if (d.isBase64) return Buffer.from(d.data, "base64").toString("utf8");
322
486
  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);
487
+ return decodeURIComponent(d.data);
488
+ } catch (_) {
489
+ return d.data;
374
490
  }
375
491
  }
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
- }
492
+ if (/^(image\/(png|jpe?g|webp|bmp|gif|x-icon|vnd\.microsoft\.icon|ico)|video\/.*|audio\/.*)$/i.test(d.mime)) {
493
+ if (d.isBase64) return Buffer.from(d.data, "base64");
494
+ }
495
+ if (!d.isBase64) {
496
+ try {
497
+ return decodeURIComponent(d.data);
498
+ } catch (_) {
499
+ return d.data;
475
500
  }
476
501
  }
477
- } catch (error) {
478
- res.writeHead(400, { "Content-Type": "text/plain" });
479
- res.end("Invalid host format");
480
502
  }
481
503
  }
504
+
505
+ // Plain base64-only string
506
+ if (isBase64Only(src)) {
507
+ if (
508
+ contentType.startsWith("image/") ||
509
+ contentType.startsWith("font/") ||
510
+ contentType.startsWith("video/") ||
511
+ contentType.startsWith("audio/") ||
512
+ contentType === "application/octet-stream"
513
+ ) {
514
+ return Buffer.from(src, "base64");
515
+ }
516
+ return src;
517
+ }
518
+
519
+ // HTML rendering
520
+ if (contentType === "text/html") {
521
+ // Adapter Closure to independently fetch isolated templates/chunks for SSR
522
+ const recursiveResolve = async (nestedPathname) => {
523
+ const resolved = await resolveFile({
524
+ pathname: nestedPathname,
525
+ hostname: urlObject.hostname,
526
+ organizationId: organization._id,
527
+ crud: activeCrud
528
+ });
529
+ return resolved?.file || null;
530
+ };
531
+
532
+ return await render.HTML({
533
+ file,
534
+ organization,
535
+ urlObject,
536
+ langRegion,
537
+ lang,
538
+ theme,
539
+ resolveFile: recursiveResolve,
540
+ crud: activeCrud
541
+ });
542
+ }
543
+
544
+ // XML host replacement
545
+ if ((contentType === "text/xml" || contentType === "application/xml") && typeof src === "string") {
546
+ const protocol = "https://";
547
+ return src.replaceAll("{{$host}}", `${protocol}${urlObject.hostname}`);
548
+ }
549
+
550
+ return src;
482
551
  }
483
552
 
484
- module.exports = CoCreateFileSystem;
553
+ /**
554
+ * Handles setting response headers, caching guidelines, bandwidth tracking, and returning binary strings.
555
+ */
556
+ export async function sendResponse({ res, src, statusCode, headers, file, organizationId, crud, organizationCacheControl }) {
557
+ try {
558
+ const activeCrud = crud || (server ? server.crud : null);
559
+ let cacheControl;
560
+ if (statusCode >= 400) {
561
+ cacheControl = "no-cache, no-store, must-revalidate";
562
+ } else if (file && file["cache-control"] !== undefined && file["cache-control"] !== null) {
563
+ const val = file["cache-control"];
564
+ if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
565
+ cacheControl = `public, max-age=${String(val).trim()};`;
566
+ } else {
567
+ cacheControl = val;
568
+ }
569
+ } else {
570
+ cacheControl = organizationCacheControl || "public, max-age=3600";
571
+ }
572
+
573
+ headers["Cache-Control"] = cacheControl;
574
+ headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
575
+ headers["Accept-CH-Lifetime"] = "86400";
576
+ headers["Vary"] = headers["Vary"]
577
+ ? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
578
+ : "Sec-CH-Prefers-Color-Scheme";
579
+
580
+ if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
581
+ // Binary formats passed directly
582
+ } else if (typeof src === "object") {
583
+ src = JSON.stringify(src);
584
+ }
585
+
586
+ const wsManager = server ? server.wsManager : null;
587
+ if (organizationId && activeCrud && wsManager) {
588
+ wsManager.emit("setBandwidth", {
589
+ type: "out",
590
+ data: src,
591
+ organization_id: organizationId
592
+ });
593
+ }
594
+ res.writeHead(statusCode, headers);
595
+ res.end(src);
596
+ } catch (error) {
597
+ console.error("Error finalizing response payload:", error);
598
+ }
599
+ }
600
+ const FileServer = {
601
+ init,
602
+ send,
603
+ resolveFile,
604
+ getDefaultFile,
605
+ getWildcardPath,
606
+ detectTheme,
607
+ handleTrailingSlash,
608
+ parseLanguage,
609
+ normalizeSrc,
610
+ sendResponse
611
+ }
612
+ // Default export added to enforce standard import convention across the platform
613
+ export default FileServer;