@cocreate/file-server 1.19.1 → 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.
- package/.github/workflows/automated.yml +23 -36
- package/CHANGELOG.md +26 -0
- package/package.json +8 -4
- package/release.config.js +12 -4
- package/src/index.js +556 -429
|
@@ -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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 }}"
|
|
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,29 @@
|
|
|
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
|
+
|
|
1
27
|
## [1.19.1](https://github.com/CoCreate-app/CoCreate-file-server/compare/v1.19.0...v1.19.1) (2026-04-15)
|
|
2
28
|
|
|
3
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/file-server",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"
|
|
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
|
-
|
|
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/
|
|
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
|
-
*
|
|
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,467 +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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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, Icons, Video, and Audio -> 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)|video\/.*|audio\/.*)$/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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
contentType.startsWith("image/") ||
|
|
277
|
-
contentType.startsWith("font/") ||
|
|
278
|
-
contentType.startsWith("video/") ||
|
|
279
|
-
contentType.startsWith("audio/") ||
|
|
280
|
-
contentType === "application/octet-stream"
|
|
281
|
-
) {
|
|
282
|
-
return Buffer.from(src, "base64");
|
|
283
|
-
}
|
|
284
|
-
// textual content kept as-is
|
|
285
|
-
return src;
|
|
286
|
-
}
|
|
305
|
+
if (defaultFile?.object?.[0]?.src) {
|
|
306
|
+
return defaultFile;
|
|
307
|
+
}
|
|
287
308
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
file,
|
|
292
|
-
organization,
|
|
293
|
-
urlObject,
|
|
294
|
-
langRegion,
|
|
295
|
-
lang,
|
|
296
|
-
theme
|
|
297
|
-
);
|
|
298
|
-
}
|
|
309
|
+
// Search global scope platform database
|
|
310
|
+
queryData.$filter.query.host.$in = ["*"];
|
|
311
|
+
queryData.organization_id = process.env.organization_id;
|
|
299
312
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return src.replaceAll("{{$host}}", `${protocol}${hostname}`);
|
|
304
|
-
}
|
|
313
|
+
if (fileName.startsWith("/admin")) {
|
|
314
|
+
queryData.$filter.query.pathname = "/superadmin" + fileName.replace("/admin", "");
|
|
315
|
+
}
|
|
305
316
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
309
333
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
}
|
|
322
429
|
|
|
323
|
-
|
|
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");
|
|
324
486
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
} else if (
|
|
329
|
-
file &&
|
|
330
|
-
file["cache-control"] !== undefined &&
|
|
331
|
-
file["cache-control"] !== null
|
|
332
|
-
) {
|
|
333
|
-
const val = file["cache-control"];
|
|
334
|
-
if (typeof val === "number" || /^\s*\d+\s*$/.test(val)) {
|
|
335
|
-
// If it's numeric (number or numeric string) treat it as max-age.
|
|
336
|
-
cacheControl = `public, max-age=${String(
|
|
337
|
-
val
|
|
338
|
-
).trim()};`;
|
|
339
|
-
} else {
|
|
340
|
-
// use the value verbatim (allow full Cache-Control strings like "no-cache" or "public, max-age=600")
|
|
341
|
-
cacheControl = val;
|
|
342
|
-
}
|
|
343
|
-
} else {
|
|
344
|
-
cacheControl = organization["cache-control"] || "public, max-age=3600";
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Always override/set Cache-Control header so the response aligns with the file metadata/defaults
|
|
348
|
-
headers["Cache-Control"] = cacheControl;
|
|
349
|
-
// Advertise that we accept the Sec-CH-Prefers-Color-Scheme client hint.
|
|
350
|
-
// After the browser sees this in a response it may include
|
|
351
|
-
// the Sec-CH-Prefers-Color-Scheme header on subsequent requests.
|
|
352
|
-
headers["Accept-CH"] = "Sec-CH-Prefers-Color-Scheme";
|
|
353
|
-
// optional: tell browser how long to remember this preference (seconds)
|
|
354
|
-
headers["Accept-CH-Lifetime"] = "86400";
|
|
355
|
-
// ensure caches/proxies vary responses by this hint
|
|
356
|
-
headers["Vary"] = headers["Vary"]
|
|
357
|
-
? headers["Vary"] + ", Sec-CH-Prefers-Color-Scheme"
|
|
358
|
-
: "Sec-CH-Prefers-Color-Scheme";
|
|
359
|
-
|
|
360
|
-
if (src instanceof Uint8Array || Buffer.isBuffer(src)) {
|
|
361
|
-
// Ensure binary data is sent as-is
|
|
362
|
-
} else if (typeof src === "object") {
|
|
363
|
-
src = JSON.stringify(src);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (organization_id)
|
|
367
|
-
crud.wsManager.emit("setBandwidth", {
|
|
368
|
-
type: "out",
|
|
369
|
-
data: src,
|
|
370
|
-
organization_id
|
|
371
|
-
});
|
|
372
|
-
res.writeHead(statusCode, headers);
|
|
373
|
-
return res.end(src);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
console.log(error);
|
|
487
|
+
return decodeURIComponent(d.data);
|
|
488
|
+
} catch (_) {
|
|
489
|
+
return d.data;
|
|
376
490
|
}
|
|
377
491
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
defaultFile &&
|
|
388
|
-
defaultFile.object &&
|
|
389
|
-
defaultFile.object[0] &&
|
|
390
|
-
defaultFile.object[0].src
|
|
391
|
-
) {
|
|
392
|
-
return defaultFile;
|
|
393
|
-
} else {
|
|
394
|
-
data.$filter.query.host.$in = ["*"];
|
|
395
|
-
data.organization_id = process.env.organization_id;
|
|
396
|
-
|
|
397
|
-
if (fileName.startsWith("/admin"))
|
|
398
|
-
data.$filter.query.pathname =
|
|
399
|
-
"/superadmin" + fileName.replace("/admin", "");
|
|
400
|
-
|
|
401
|
-
defaultFile = await crud.send(data);
|
|
402
|
-
|
|
403
|
-
if (fileName !== "/hostNotFound.html") {
|
|
404
|
-
crud.wsManager.emit("setBandwidth", {
|
|
405
|
-
type: "out",
|
|
406
|
-
data,
|
|
407
|
-
organization_id
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
crud.wsManager.emit("setBandwidth", {
|
|
411
|
-
type: "in",
|
|
412
|
-
data: defaultFile,
|
|
413
|
-
organization_id
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
defaultFile &&
|
|
419
|
-
defaultFile.object &&
|
|
420
|
-
defaultFile.object[0] &&
|
|
421
|
-
defaultFile.object[0].src
|
|
422
|
-
) {
|
|
423
|
-
if (fileName.startsWith("/admin")) {
|
|
424
|
-
data.object[0].directory = "admin";
|
|
425
|
-
data.object[0].path =
|
|
426
|
-
"/admin" +
|
|
427
|
-
data.object[0].path.replace("/superadmin", "");
|
|
428
|
-
data.object[0].pathname = fileName;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
crud.send({
|
|
432
|
-
method: "object.create",
|
|
433
|
-
host: hostname,
|
|
434
|
-
array: "files",
|
|
435
|
-
object: defaultFile.object[0],
|
|
436
|
-
organization_id
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
return defaultFile;
|
|
440
|
-
} else {
|
|
441
|
-
switch (fileName) {
|
|
442
|
-
case "/403.html":
|
|
443
|
-
defaultFile.object = [
|
|
444
|
-
{
|
|
445
|
-
src: `${pathname} access not allowed for ${organization_id}`
|
|
446
|
-
}
|
|
447
|
-
];
|
|
448
|
-
break;
|
|
449
|
-
case "/404.html":
|
|
450
|
-
defaultFile.object = [
|
|
451
|
-
{
|
|
452
|
-
src: `${pathname} could not be found for ${organization_id}`
|
|
453
|
-
}
|
|
454
|
-
];
|
|
455
|
-
break;
|
|
456
|
-
case "/balanceFalse.html":
|
|
457
|
-
defaultFile.object = [
|
|
458
|
-
{
|
|
459
|
-
src: "This organizations account balance has fallen bellow 0: "
|
|
460
|
-
}
|
|
461
|
-
];
|
|
462
|
-
break;
|
|
463
|
-
case "/hostNotFound.html":
|
|
464
|
-
defaultFile.object = [
|
|
465
|
-
{
|
|
466
|
-
src:
|
|
467
|
-
"An organization could not be found using the host: " +
|
|
468
|
-
hostname +
|
|
469
|
-
" in platformDB: " +
|
|
470
|
-
process.env.organization_id
|
|
471
|
-
}
|
|
472
|
-
];
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
return defaultFile;
|
|
476
|
-
}
|
|
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;
|
|
477
500
|
}
|
|
478
501
|
}
|
|
479
|
-
} catch (error) {
|
|
480
|
-
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
481
|
-
res.end("Invalid host format");
|
|
482
502
|
}
|
|
483
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;
|
|
484
551
|
}
|
|
485
552
|
|
|
486
|
-
|
|
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;
|