@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,107 @@
1
+ // cache: site -> pattern -> { regex, keys }
2
+ const __routeCache = new Map();
3
+
4
+ function compile(site, pattern) {
5
+
6
+ let siteMap = __routeCache.get(site);
7
+
8
+ if (!siteMap) {
9
+
10
+ siteMap = new Map();
11
+ __routeCache.set(site, siteMap);
12
+
13
+ }
14
+
15
+ let compiled = siteMap.get(pattern);
16
+
17
+ if (!compiled) {
18
+
19
+ const esc = (s) => s.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1');
20
+ const keys = [];
21
+ const re = '^/' + pattern
22
+ .split('/').filter(Boolean)
23
+ .map((seg) => {
24
+
25
+ if (seg.startsWith(':')) {
26
+
27
+ keys.push(seg.slice(1));
28
+
29
+ return '([^/]+)';
30
+
31
+ }
32
+
33
+ return esc(seg);
34
+
35
+ })
36
+ .join('/') + '/?$';
37
+
38
+ compiled = {
39
+ regex: new RegExp(re),
40
+ keys
41
+ };
42
+ siteMap.set(pattern, compiled);
43
+
44
+ }
45
+
46
+ return compiled;
47
+
48
+ }
49
+
50
+ export function matchPathSite(site, pattern, path) {
51
+
52
+ const {
53
+ regex, keys
54
+ } = compile(site, pattern);
55
+ const m = regex.exec(path);
56
+
57
+ if (!m) return null;
58
+
59
+ const params = {};
60
+
61
+ for (let i = 0; i < keys.length; i++) params[keys[i]] = m[i + 1];
62
+
63
+ return params;
64
+
65
+ }
66
+
67
+ export async function specialPaths(awesomenessRequest, routes) {
68
+
69
+ for (const route of routes) {
70
+
71
+ // console.log('Checking route:', {
72
+ // pattern: route.pattern,
73
+ // path: awesomenessRequest.path,
74
+ // site: awesomenessRequest.site
75
+ // });
76
+
77
+
78
+ const params = matchPathSite(awesomenessRequest.site, route.pattern, awesomenessRequest.path);
79
+
80
+ if (params) {
81
+
82
+ // console.log('Matched route:', {
83
+ // pattern: route.pattern,
84
+ // path: awesomenessRequest.path,
85
+ // site: awesomenessRequest.site,
86
+ // params
87
+ // });
88
+
89
+ await route.handler(awesomenessRequest, params, route);
90
+
91
+ return; // stop the chain here
92
+
93
+ } else {
94
+
95
+ // console.log('No match for route:', {
96
+ // pattern: route.pattern,
97
+ // path: awesomenessRequest.path,
98
+ // site: awesomenessRequest.site
99
+ // });
100
+
101
+ }
102
+
103
+ }
104
+
105
+ }
106
+
107
+ export default specialPaths;
@@ -0,0 +1,127 @@
1
+ import { clean } from '@awesomeness-js/utils';
2
+ import checkAccess from './checkAccess.js';
3
+ import { getConfig } from "../config.js";
4
+
5
+ export default async (awesomenessRequest) => {
6
+
7
+ const awesomenessConfig = getConfig();
8
+
9
+ const {
10
+ data,
11
+ routeInfo
12
+ } = awesomenessRequest;
13
+
14
+ // IS SESSION VALID
15
+
16
+ try {
17
+
18
+ await awesomenessConfig.checkSession(awesomenessRequest);
19
+
20
+
21
+ } catch(e){
22
+
23
+ if(
24
+ routeInfo?.permissions.length
25
+ && !routeInfo?.permissions.includes('*')
26
+ ){
27
+
28
+ if(
29
+ process.env.NODE_ENV === 'development'
30
+ && awesomenessConfig.byPassAccessRequirementsInDev === true
31
+ ){
32
+
33
+ if(awesomenessConfig.debug){
34
+
35
+ console.log('by passing access requirement: NODE_ENV is development && byPassAccessRequirementsInDev is true');
36
+
37
+ }
38
+
39
+ } else {
40
+
41
+ throw {
42
+ message: 'session check failed',
43
+ error: e
44
+ };
45
+
46
+ }
47
+
48
+
49
+ }
50
+
51
+ }
52
+
53
+
54
+ if(routeInfo?.permissions.length){
55
+
56
+ await checkAccess({
57
+ permissionsAllowed: routeInfo.permissions,
58
+ awesomenessRequest,
59
+ });
60
+
61
+ }
62
+
63
+ // DOES USER HAVE THE RIGHT PERMISSIONS
64
+
65
+ // CLEAN THE DATA
66
+ try {
67
+
68
+ if(!routeInfo.properties){
69
+
70
+ // nothing to clean
71
+ return true;
72
+
73
+ }
74
+
75
+ if(typeof data === 'object' && Object.keys(data).length){
76
+
77
+ const testing = data.testing || false;
78
+
79
+ delete data.testing;
80
+
81
+ if(awesomenessConfig.debug){
82
+
83
+ console.log({ data });
84
+
85
+ }
86
+
87
+ let cleanedObject = clean.object(data, routeInfo.properties, {
88
+ testMode: awesomenessConfig.debug,
89
+ });
90
+
91
+
92
+ data.testing = testing;
93
+
94
+ if(!awesomenessRequest.cleanData){
95
+
96
+ awesomenessRequest.cleanData = {};
97
+
98
+ }
99
+
100
+ // awesomenessRequest.cleanData = data;
101
+
102
+ awesomenessRequest.cleanData = {
103
+ ... awesomenessRequest.cleanData,
104
+ ... cleanedObject
105
+ };
106
+
107
+ }
108
+
109
+
110
+ return true;
111
+
112
+
113
+
114
+ } catch (err) {
115
+
116
+ throw {
117
+ message: 'validation error',
118
+ err: {
119
+ details: err,
120
+ message: err.message,
121
+ stack: err.stack
122
+ }
123
+ };
124
+
125
+ }
126
+
127
+ };
@@ -0,0 +1,67 @@
1
+ import { getConfig } from "../../config.js";
2
+
3
+ export function handleWsMessage({
4
+ socket,
5
+ raw
6
+ }) {
7
+
8
+ const awesomenessConfig = getConfig();
9
+
10
+ let msg;
11
+
12
+ try {
13
+
14
+ msg = JSON.parse(raw.toString());
15
+
16
+ } catch (err) {
17
+
18
+ socket.send(JSON.stringify({
19
+ type: "error",
20
+ error: "Invalid JSON",
21
+ ts: Date.now()
22
+ }));
23
+
24
+ return;
25
+
26
+ }
27
+
28
+ if (awesomenessConfig.debug) {
29
+
30
+ // Example: log which client sent the message
31
+ console.log("client", socket.clientId, "sent", msg);
32
+
33
+ }
34
+
35
+ if (msg?.type === "ping") {
36
+
37
+ socket.send(JSON.stringify({
38
+ type: "pong",
39
+ clientId: socket.clientId,
40
+ echo: msg.data ?? null,
41
+ ts: Date.now()
42
+ }));
43
+
44
+ return;
45
+
46
+ }
47
+
48
+ if(typeof awesomenessConfig.wsHandler === 'function'){
49
+
50
+ awesomenessConfig.wsHandler({
51
+ socket,
52
+ message: msg,
53
+ });
54
+
55
+ } else {
56
+
57
+ socket.send(JSON.stringify({
58
+ type: msg.type ?? "echo",
59
+ clientId: socket.clientId,
60
+ echo: msg,
61
+ ts: Date.now()
62
+ }));
63
+
64
+ }
65
+
66
+
67
+ }
@@ -0,0 +1,50 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { handleWsMessage } from "./handlers.js";
3
+
4
+ export function attachWs(server, path = "/ws") {
5
+
6
+ // noServer mode so we own upgrades
7
+ const wss = new WebSocketServer({ noServer: true });
8
+
9
+ wss.on("connection", (socket, req) => {
10
+
11
+ const clientId = Math.random().toString(36).slice(2, 8);
12
+
13
+ socket.clientId = clientId;
14
+
15
+ socket.send(JSON.stringify({
16
+ type: "welcome",
17
+ clientId,
18
+ ts: Date.now()
19
+ }));
20
+
21
+ socket.on("message", (raw) => handleWsMessage({
22
+ socket,
23
+ raw,
24
+ }));
25
+
26
+ });
27
+
28
+ server.on("upgrade", (req, socket, head) => {
29
+
30
+ const { url } = req;
31
+
32
+ if (url === path) {
33
+
34
+ wss.handleUpgrade(req, socket, head, (ws) => {
35
+
36
+ wss.emit("connection", ws, req);
37
+
38
+ });
39
+
40
+ } else {
41
+
42
+ socket.destroy();
43
+
44
+ }
45
+
46
+ });
47
+
48
+ return wss;
49
+
50
+ }
package/start.js ADDED
@@ -0,0 +1,122 @@
1
+ import Koa from 'koa';
2
+ import compress from 'koa-compress';
3
+ import http from "http";
4
+
5
+ import { attachAwesomenessRequest } from './server/koa/attachAwesomenessRequest.js';
6
+ import { cors } from './server/koa/cors.js';
7
+ import { errorHandler } from './server/koa/errorHandler.js';
8
+ import { jsonBodyParser } from './server/koa/jsonBodyParser.js';
9
+ import { serverUp } from './server/koa/serverUp.js';
10
+ import { timeout } from './server/koa/timeout.js';
11
+ import { staticFiles } from "./server/koa/staticFiles.js";
12
+ import { routeRequest } from './server/koa/routeRequest.js';
13
+ import './server/errors.js'; // handle errors
14
+
15
+ import { attachWs } from "./server/ws/index.js";
16
+
17
+ import { getConfig } from './config.js';
18
+
19
+
20
+ export default async function start(){
21
+
22
+ const awesomenessConfig = getConfig();
23
+
24
+ console.log(`Starting Awesomeness server on port ${process.env.PORT}`);
25
+
26
+ // Koa app
27
+ const app = new Koa();
28
+
29
+ app.use(compress());
30
+
31
+ // error handling
32
+ app.use(errorHandler);
33
+
34
+ // limits
35
+ app.use(jsonBodyParser);
36
+ app.use(timeout(1000 * 60 * 5)); // 5 minutes
37
+
38
+ // enable cors
39
+ app.use(cors);
40
+
41
+ // where we going
42
+ app.use(attachAwesomenessRequest);
43
+
44
+ // default static files
45
+ app.use(staticFiles);
46
+
47
+ if(Array.isArray(awesomenessConfig.beforeRouteMiddleware)){
48
+
49
+ awesomenessConfig.beforeRouteMiddleware.forEach((middlewareFunction) => {
50
+
51
+ app.use(middlewareFunction);
52
+
53
+ });
54
+
55
+ } else {
56
+
57
+ if(awesomenessConfig.debug){
58
+
59
+ console.log('No customMiddleware array found in awesomenessConfig');
60
+
61
+ }
62
+
63
+ }
64
+
65
+
66
+ // dynamic routing
67
+ app.use(routeRequest);
68
+
69
+ // create HTTP server manually so WS can hook upgrade
70
+
71
+ const server = http.createServer(app.callback());
72
+
73
+ if(typeof awesomenessConfig.wsHandler === 'function'){
74
+
75
+ attachWs(server);
76
+
77
+ } else {
78
+
79
+ console.log('No wsHandler function found in awesomenessConfig');
80
+
81
+ }
82
+
83
+ // init DB
84
+ if(typeof awesomenessConfig.initDB === 'function'){
85
+
86
+ await awesomenessConfig.initDB();
87
+
88
+ } else {
89
+
90
+ console.log('No initDB function found in awesomenessConfig');
91
+
92
+ }
93
+
94
+
95
+ server.listen(process.env.PORT, serverUp);
96
+
97
+
98
+ [ 'SIGINT', 'SIGTERM' ].forEach(async (signal) => {
99
+
100
+ process.on(signal, async () => {
101
+
102
+ console.log(`Received ${signal}, shutting down gracefully...`);
103
+ server.close(() => {
104
+
105
+ console.log('Server closed');
106
+ process.exit(0);
107
+
108
+ });
109
+
110
+ setTimeout(() => {
111
+
112
+ console.error('Force shutdown');
113
+ process.exit(1);
114
+
115
+ }, 5000);
116
+
117
+ });
118
+
119
+ });
120
+
121
+
122
+ }
@@ -0,0 +1,15 @@
1
+ export default {
2
+ test: {
3
+ globals: true,
4
+ cache: false,
5
+ environment: 'node',
6
+ watchExclude: [
7
+ '**/node_modules/**',
8
+ '**/.git/**',
9
+ ],
10
+ testMatch: [
11
+ '**/*.test.js'
12
+ ],
13
+ setupFiles: './tests/setup.js',
14
+ }
15
+ };