@awesomeness-js/server 1.0.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.
Files changed (51) hide show
  1. package/.editorconfig +4 -0
  2. package/.gitattributes +2 -0
  3. package/.jshintrc +3 -0
  4. package/.vscode/settings.json +17 -0
  5. package/CLA.md +9 -0
  6. package/CONTRIBUTING.md +18 -0
  7. package/LICENSE +13 -0
  8. package/NOTICE +15 -0
  9. package/README.md +15 -0
  10. package/SECURITY.md +7 -0
  11. package/config.js +29 -0
  12. package/eslint.config.js +101 -0
  13. package/example-.awesomeness/applicationMap.js +4 -0
  14. package/example-.awesomeness/beforeRouteMiddleware.js +15 -0
  15. package/example-.awesomeness/checkSession.js +15 -0
  16. package/example-.awesomeness/config.js +40 -0
  17. package/example-.awesomeness/hostMap.js +44 -0
  18. package/example-.awesomeness/initDB.js +65 -0
  19. package/example-.awesomeness/setupDB/applications.js +47 -0
  20. package/example-.awesomeness/setupDB/users.js +65 -0
  21. package/example-.awesomeness/setupDB/websites.js +49 -0
  22. package/example-.awesomeness/specialRoutes.js +7 -0
  23. package/example-.awesomeness/wsHandler.js +13 -0
  24. package/index.js +22 -0
  25. package/package.json +34 -0
  26. package/server/applicationMap.js +33 -0
  27. package/server/awesomenessNormalizeRequest.js +131 -0
  28. package/server/brotliJsonResponse.js +28 -0
  29. package/server/checkAccess.js +34 -0
  30. package/server/componentDependencies.js +301 -0
  31. package/server/errors.js +11 -0
  32. package/server/fetchPage.js +269 -0
  33. package/server/getMD.js +22 -0
  34. package/server/koa/attachAwesomenessRequest.js +24 -0
  35. package/server/koa/cors.js +22 -0
  36. package/server/koa/errorHandler.js +32 -0
  37. package/server/koa/finalFormat.js +34 -0
  38. package/server/koa/jsonBodyParser.js +172 -0
  39. package/server/koa/routeRequest.js +288 -0
  40. package/server/koa/serverUp.js +7 -0
  41. package/server/koa/staticFiles.js +97 -0
  42. package/server/koa/timeout.js +42 -0
  43. package/server/pageInfo.js +121 -0
  44. package/server/reRoute.js +54 -0
  45. package/server/resolveRealCasePath.js +56 -0
  46. package/server/specialPaths.js +107 -0
  47. package/server/validateRequest.js +127 -0
  48. package/server/ws/handlers.js +67 -0
  49. package/server/ws/index.js +50 -0
  50. package/start.js +122 -0
  51. package/vitest.config.js +15 -0
@@ -0,0 +1,269 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+
4
+ import { componentDependencies } from "./componentDependencies.js";
5
+ import pageInfo from "./pageInfo.js";
6
+ import { each, md5, combineFiles } from "@awesomeness-js/utils";
7
+ import { getConfig } from "../config.js";
8
+
9
+ const componentNamespace = "ui";
10
+ const pageNamespaceBase = `app.pages`;
11
+
12
+ export default async function fetchPage(
13
+ awesomenessRequest,
14
+ {
15
+ about,
16
+ cssPath,
17
+ jsPath,
18
+ getData,
19
+ showDetails = false,
20
+ page = null
21
+ } = {}
22
+ ) {
23
+
24
+ const awesomenessConfig = getConfig();
25
+
26
+ // normalize siteURL (expected to point at the /sites/ directory)
27
+ const sitesRootPath =
28
+ awesomenessConfig.siteURL instanceof URL
29
+ ? fileURLToPath(awesomenessConfig.siteURL)
30
+ : awesomenessConfig.siteURL;
31
+
32
+ const sitePagesRoot = path.join(sitesRootPath, awesomenessRequest.site, "pages");
33
+
34
+ function pageFn(filePath){
35
+
36
+ // make file path relative to ".../sites/<site>/pages/"
37
+ let rel = path.relative(sitePagesRoot, filePath);
38
+
39
+ // normalize to posix-style so splitting works on Windows + *nix
40
+ rel = rel.split(path.sep).join("/");
41
+
42
+ // remove extension and split on /js/
43
+ const parts = rel.replace(/\.js$/i, "").split("/js/");
44
+
45
+ const pagePath = parts[0] || "";
46
+ const fnPath = (parts[1] || "").split("/").filter(Boolean).join(".");
47
+
48
+ const pageNamespace = `${pageNamespaceBase}.${pagePath.split("/").filter(Boolean).join(".")}`;
49
+ const pageFnName = `${pageNamespace}.${fnPath}`;
50
+
51
+ return pageFnName;
52
+
53
+ }
54
+
55
+
56
+ // initialize if not already available
57
+ if (!awesomenessRequest.updatedMeta) {
58
+
59
+ awesomenessRequest.updatedMeta = {};
60
+
61
+ }
62
+
63
+ let forceRefresh = false;
64
+
65
+ if (!page) {
66
+
67
+ page = awesomenessRequest.pageRoute;
68
+
69
+ } else {
70
+
71
+ forceRefresh = true;
72
+
73
+ }
74
+
75
+ if (!about || !cssPath || !jsPath || !getData) {
76
+
77
+ let info = await pageInfo(awesomenessRequest, { page });
78
+
79
+ cssPath = info.cssPath;
80
+ jsPath = info.jsPath;
81
+ about = info.about;
82
+ getData = info.getData;
83
+
84
+ if (info.mdContent) {
85
+
86
+ page = "_md";
87
+
88
+ }
89
+
90
+ }
91
+
92
+ if (!about || !cssPath || !jsPath) {
93
+
94
+ throw {
95
+ reason: "page not found",
96
+ aboutPath,
97
+ cssPath,
98
+ jsPath,
99
+ awesomenessRequest,
100
+ };
101
+
102
+ }
103
+
104
+ const meta = {
105
+ about,
106
+ pages: {},
107
+ components: {},
108
+ };
109
+
110
+ // CHECK PERMISSIONS
111
+ if (about?.permissions.length) {
112
+
113
+ const userPermissions = awesomenessRequest.user?.permissions || [];
114
+ const hasPermission = about.permissions.some((rp) => userPermissions.includes(rp) || rp === "*");
115
+
116
+ if (!hasPermission) {
117
+
118
+ if (process.env.NODE_ENV === "development") {
119
+
120
+ console.log("by page passing access requirement.");
121
+
122
+ } else {
123
+
124
+ throw {
125
+ message: "user does not have permission to view this page",
126
+ permissions: about.permissions,
127
+ userPermissions,
128
+ };
129
+
130
+ }
131
+
132
+ }
133
+
134
+ }
135
+
136
+ // GET PAGE
137
+ if (
138
+ forceRefresh === true ||
139
+ awesomenessRequest.testing === true ||
140
+ about.version != awesomenessRequest.meta?.pages[page]
141
+ ) {
142
+
143
+ meta.pages[page] = meta.pages[page] || {};
144
+
145
+ meta.pages[page].version = about.version;
146
+ meta.pages[page].about = about;
147
+
148
+ // GET ALL PAGE SCRIPTS
149
+ try {
150
+
151
+ let js = "";
152
+
153
+ js += combineFiles(jsPath, "js", {
154
+ processContent: ({
155
+ content, path
156
+ }) => {
157
+
158
+ const fnName = pageFn(path, awesomenessRequest);
159
+
160
+ content = content.replaceAll(`import ui from '#ui';`, "");
161
+ content = content.replaceAll(`import ui from "#ui";`, "");
162
+
163
+ content = content.replaceAll("export default function", `${fnName} = function`);
164
+ content = content.replaceAll("export default async function", `${fnName} = async function`);
165
+ content = content.replaceAll("export default async", `${fnName} = async`);
166
+ content = content.replaceAll("export default", `${fnName} =`);
167
+
168
+ return content;
169
+
170
+ },
171
+ });
172
+
173
+ meta.pages[page].js = js;
174
+
175
+ } catch (err) {
176
+
177
+ console.log("failed to get page js", { err });
178
+
179
+ }
180
+
181
+ // GET ALL PAGE CSS
182
+ try {
183
+
184
+ let css = "";
185
+
186
+ css += combineFiles(cssPath, "css");
187
+ meta.pages[page].css = css;
188
+
189
+ } catch (err) {
190
+
191
+ meta.pages[page].css = "/* no css found */";
192
+
193
+ }
194
+
195
+ }
196
+
197
+ if (about?.components?.length) {
198
+
199
+ const allDependencies = componentDependencies(about.components, {
200
+ componentLocations: awesomenessConfig.componentLocations(awesomenessRequest),
201
+ namespace: componentNamespace,
202
+ showDetails,
203
+ });
204
+
205
+ each(allDependencies, (data, component) => {
206
+
207
+ try {
208
+
209
+ const css = data.css;
210
+ const js = data.js;
211
+ let hash = md5(css + js);
212
+
213
+ if (hash !== awesomenessRequest.meta.components[component]) {
214
+
215
+ meta.components[component] = {
216
+ css,
217
+ js,
218
+ hash
219
+ };
220
+
221
+ if (showDetails) {
222
+
223
+ meta.components[component].css_details = data.css_details;
224
+ meta.components[component].js_details = data.js_details;
225
+
226
+ }
227
+
228
+ }
229
+
230
+ } catch (err) {
231
+
232
+ console.log(`failed to get component: ${component}`, {
233
+ component,
234
+ err
235
+ });
236
+
237
+ }
238
+
239
+ });
240
+
241
+ }
242
+
243
+ const pages = awesomenessRequest.updatedMeta.pages || {};
244
+ const components = awesomenessRequest.updatedMeta.components || {};
245
+
246
+ if (Object.keys(meta.pages).length) {
247
+
248
+ awesomenessRequest.updatedMeta.pages = {
249
+ ...pages,
250
+ ...meta.pages
251
+ };
252
+
253
+ }
254
+
255
+ if (Object.keys(meta.components).length) {
256
+
257
+ awesomenessRequest.updatedMeta.components = {
258
+ ...components,
259
+ ...meta.components
260
+ };
261
+
262
+ }
263
+
264
+ const pageData = await getData(awesomenessRequest);
265
+
266
+
267
+ return pageData;
268
+
269
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile } from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ export default async function getMD(relativePath, callerUrl) {
6
+
7
+ try {
8
+
9
+ const callerDir = path.dirname(fileURLToPath(callerUrl));
10
+ const fullPath = path.resolve(callerDir, relativePath);
11
+
12
+ console.log(`Loading MD file from: ${fullPath}`);
13
+
14
+ return await readFile(fullPath, 'utf8');
15
+
16
+ } catch {
17
+
18
+ return null;
19
+
20
+ }
21
+
22
+ }
@@ -0,0 +1,24 @@
1
+ import awesomenessNormalizeRequest from '../awesomenessNormalizeRequest.js';
2
+ import specialPaths from '../specialPaths.js';
3
+ import { getConfig } from "../../config.js";
4
+
5
+ async function attachAwesomenessRequest(ctx, next) {
6
+
7
+ const awesomenessConfig = getConfig();
8
+
9
+ ctx.awesomenessRequest = await awesomenessNormalizeRequest({ req: ctx });
10
+
11
+ const routes = awesomenessConfig.specialRoutes[ctx.awesomenessRequest.site] || [];
12
+
13
+ if(ctx.awesomenessRequest.awesomenessType === 'page'){
14
+
15
+ await specialPaths(ctx.awesomenessRequest, routes);
16
+
17
+ }
18
+
19
+ await next();
20
+
21
+ }
22
+
23
+ export { attachAwesomenessRequest };
24
+ export default attachAwesomenessRequest;
@@ -0,0 +1,22 @@
1
+ const cors = async (ctx, next) => {
2
+
3
+ // Set CORS headers
4
+ ctx.set('Access-Control-Allow-Origin', '*');
5
+ ctx.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
6
+ ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
7
+
8
+ // Handle OPTIONS requests
9
+ if (ctx.method === 'OPTIONS') {
10
+
11
+ ctx.status = 204; // No Content
12
+
13
+ return;
14
+
15
+ }
16
+
17
+ // Proceed to the next middleware
18
+ await next();
19
+
20
+ };
21
+
22
+ export { cors };
@@ -0,0 +1,32 @@
1
+ const errorHandler = async (ctx, next) => {
2
+
3
+ try {
4
+
5
+ await next();
6
+
7
+ } catch (err) {
8
+
9
+ if (err.code === 'EPIPE') {
10
+
11
+ console.error('EPIPE error: Client closed connection');
12
+ ctx.status = 499; // Client Closed Request
13
+ ctx.body = 'Client closed connection unexpectedly';
14
+
15
+ return;
16
+
17
+ } else {
18
+
19
+ // Handle other errors normally
20
+ ctx.status = err.status || 500;
21
+ ctx.body = err.message || 'Internal Server Error';
22
+ ctx.app.emit('error', err, ctx);
23
+
24
+ return;
25
+
26
+ }
27
+
28
+ }
29
+
30
+ };
31
+
32
+ export { errorHandler };
@@ -0,0 +1,34 @@
1
+ function finalFormat(awesomenessRequest, ctx){
2
+
3
+ if(!ctx.body.meta){
4
+
5
+ ctx.body.meta = {};
6
+
7
+ }
8
+
9
+ if(awesomenessRequest.pageInit){
10
+
11
+ ctx.body.meta.pageInit = awesomenessRequest.pageInit;
12
+
13
+ }
14
+
15
+ // just a test
16
+ if(process.env.TESTING){
17
+
18
+ ctx.body.awesomenessRequest = awesomenessRequest;
19
+
20
+ }
21
+
22
+ // meta update user
23
+ if(awesomenessRequest.user) {
24
+
25
+ ctx.body.meta.user = awesomenessRequest.user;
26
+
27
+ }
28
+
29
+ return;
30
+
31
+ }
32
+
33
+ export { finalFormat };
34
+ export default finalFormat;
@@ -0,0 +1,172 @@
1
+ import Busboy from 'busboy';
2
+
3
+ // ---------- helper for JSON ----------
4
+ const parseBody = (req, limit) => {
5
+
6
+ return new Promise((resolve, reject) => {
7
+
8
+ let body = '';
9
+ let receivedLength = 0;
10
+
11
+ req.on('data', (chunk) => {
12
+
13
+ receivedLength += chunk.length;
14
+
15
+ if (receivedLength > limit) {
16
+
17
+ reject(new Error('Payload too large'));
18
+ req.destroy();
19
+
20
+ return;
21
+
22
+ }
23
+
24
+ body += chunk;
25
+
26
+ });
27
+
28
+ req.on('end', () => resolve(body));
29
+ req.on('error', (err) => reject(err));
30
+
31
+ });
32
+
33
+ };
34
+
35
+ // ---------- multipart with Busboy (in memory, preserves field structure) ----------
36
+ const parseMultipart = (req, limit = 10 * 1024 * 1024) => {
37
+
38
+ return new Promise((resolve, reject) => {
39
+
40
+ const busboy = Busboy({
41
+ headers: req.headers,
42
+ limits: { fileSize: limit }
43
+ });
44
+ const body = {};
45
+
46
+ busboy.on('field', (name, value) => {
47
+
48
+ // handle "name[]" style fields as arrays
49
+ if (name.endsWith('[]')) {
50
+
51
+ const clean = name.slice(0, -2);
52
+
53
+ body[clean] ??= [];
54
+ body[clean].push(value);
55
+
56
+ } else if (body[name] !== undefined) {
57
+
58
+ // already exists → turn into array
59
+ if (!Array.isArray(body[name])) body[name] = [ body[name] ];
60
+ body[name].push(value);
61
+
62
+ } else {
63
+
64
+ body[name] = value;
65
+
66
+ }
67
+
68
+ });
69
+
70
+ busboy.on('file', (name, file, info) => {
71
+
72
+ const {
73
+ filename, encoding, mimeType
74
+ } = info;
75
+ const chunks = [];
76
+ let totalSize = 0;
77
+
78
+ file.on('data', (chunk) => {
79
+
80
+ totalSize += chunk.length;
81
+
82
+ if (totalSize > limit) {
83
+
84
+ reject(new Error('File too large'));
85
+ req.destroy();
86
+
87
+ return;
88
+
89
+ }
90
+
91
+ chunks.push(chunk);
92
+
93
+ });
94
+
95
+ file.on('end', () => {
96
+
97
+ const fileObj = {
98
+ filename,
99
+ mimeType,
100
+ encoding,
101
+ buffer: Buffer.concat(chunks),
102
+ };
103
+
104
+ // name[] → array
105
+ if (name.endsWith('[]')) {
106
+
107
+ const clean = name.slice(0, -2);
108
+
109
+ body[clean] ??= [];
110
+ body[clean].push(fileObj);
111
+
112
+ } else if (body[name] !== undefined) {
113
+
114
+ if (!Array.isArray(body[name])) body[name] = [ body[name] ];
115
+ body[name].push(fileObj);
116
+
117
+ } else {
118
+
119
+ body[name] = fileObj;
120
+
121
+ }
122
+
123
+ });
124
+
125
+ });
126
+
127
+ busboy.on('finish', () => resolve(body));
128
+ busboy.on('error', (err) => reject(err));
129
+
130
+ req.pipe(busboy);
131
+
132
+ });
133
+
134
+ };
135
+
136
+
137
+
138
+ // ---------- middleware ----------
139
+ const jsonBodyParser = async (ctx, next) => {
140
+
141
+ const method = ctx.method;
142
+ const isJson =
143
+ [ 'POST', 'PUT', 'PATCH' ].includes(method) && ctx.is('application/json');
144
+ const isMultipart =
145
+ [ 'POST', 'PUT', 'PATCH' ].includes(method) && ctx.is('multipart/form-data');
146
+
147
+
148
+ try {
149
+
150
+ if (isJson) {
151
+
152
+ const body = await parseBody(ctx.req, 10 * 1024 * 1024);
153
+
154
+ ctx.request.body = JSON.parse(body);
155
+
156
+ } else if (isMultipart) {
157
+
158
+ ctx.request.body = await parseMultipart(ctx.req);
159
+
160
+ }
161
+
162
+ } catch (err) {
163
+
164
+ ctx.throw(413, new Error('Payload too large'));
165
+
166
+ }
167
+
168
+ await next();
169
+
170
+ };
171
+
172
+ export { jsonBodyParser };