kempo-server 1.4.3 → 1.4.5

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 (47) hide show
  1. package/.github/copilot-instructions.md +96 -96
  2. package/.github/workflows/publish-npm.yml +41 -0
  3. package/README.md +650 -650
  4. package/builtinMiddleware.js +136 -136
  5. package/defaultConfig.js +129 -129
  6. package/docs/.config.json +5 -5
  7. package/docs/.config.json.example +19 -19
  8. package/docs/api/user/[id]/GET.js +15 -15
  9. package/docs/api/user/[id]/[info]/DELETE.js +12 -12
  10. package/docs/api/user/[id]/[info]/GET.js +17 -17
  11. package/docs/api/user/[id]/[info]/POST.js +18 -18
  12. package/docs/api/user/[id]/[info]/PUT.js +19 -19
  13. package/docs/configuration.html +119 -119
  14. package/docs/examples.html +201 -201
  15. package/docs/getting-started.html +72 -72
  16. package/docs/index.html +81 -81
  17. package/docs/manifest.json +87 -87
  18. package/docs/middleware.html +147 -147
  19. package/docs/request-response.html +95 -95
  20. package/docs/routing.html +77 -77
  21. package/example-middleware.js +23 -23
  22. package/example.config.json +50 -50
  23. package/findFile.js +138 -138
  24. package/getFiles.js +72 -72
  25. package/getFlags.js +34 -34
  26. package/index.js +47 -47
  27. package/middlewareRunner.js +25 -25
  28. package/package.json +10 -6
  29. package/requestWrapper.js +87 -87
  30. package/responseWrapper.js +204 -204
  31. package/router.js +285 -285
  32. package/serveFile.js +71 -71
  33. package/tests/builtinMiddleware-cors.node-test.js +17 -17
  34. package/tests/builtinMiddleware.node-test.js +74 -74
  35. package/tests/defaultConfig.node-test.js +13 -13
  36. package/tests/example-middleware.node-test.js +31 -31
  37. package/tests/findFile.node-test.js +46 -46
  38. package/tests/getFiles.node-test.js +25 -25
  39. package/tests/getFlags.node-test.js +30 -30
  40. package/tests/index.node-test.js +23 -23
  41. package/tests/middlewareRunner.node-test.js +18 -18
  42. package/tests/requestWrapper.node-test.js +51 -51
  43. package/tests/responseWrapper.node-test.js +74 -74
  44. package/tests/router-middleware.node-test.js +46 -46
  45. package/tests/router.node-test.js +88 -88
  46. package/tests/serveFile.node-test.js +52 -52
  47. package/tests/test-utils.js +106 -106
@@ -1,136 +1,136 @@
1
- // Built-in middleware functions for Kempo Server
2
- import zlib from 'zlib';
3
-
4
- // CORS Middleware
5
- export const corsMiddleware = (config) => {
6
- return async (req, res, next) => {
7
- const origin = req.headers.origin;
8
- const allowedOrigins = Array.isArray(config.origin) ? config.origin : [config.origin];
9
-
10
- if (config.origin === '*' || allowedOrigins.includes(origin)) {
11
- res.setHeader('Access-Control-Allow-Origin', origin || '*');
12
- }
13
-
14
- res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
15
- res.setHeader('Access-Control-Allow-Headers', config.headers.join(', '));
16
-
17
- // Handle preflight requests
18
- if (req.method === 'OPTIONS') {
19
- res.writeHead(200);
20
- res.end();
21
- return;
22
- }
23
-
24
- await next();
25
- };
26
- };
27
-
28
- // Compression Middleware
29
- export const compressionMiddleware = (config) => {
30
- return async (req, res, next) => {
31
- const acceptEncoding = req.headers['accept-encoding'] || '';
32
-
33
- if (!acceptEncoding.includes('gzip')) {
34
- return await next();
35
- }
36
-
37
- const originalEnd = res.end;
38
- const originalWrite = res.write;
39
- const chunks = [];
40
-
41
- res.write = function(chunk) {
42
- if (chunk) chunks.push(Buffer.from(chunk));
43
- return true;
44
- };
45
-
46
- res.end = function(chunk) {
47
- if (chunk) chunks.push(Buffer.from(chunk));
48
-
49
- const buffer = Buffer.concat(chunks);
50
-
51
- // Only compress if above threshold
52
- if (buffer.length >= config.threshold) {
53
- zlib.gzip(buffer, (err, compressed) => {
54
- if (!err && compressed.length < buffer.length) {
55
- res.setHeader('Content-Encoding', 'gzip');
56
- res.setHeader('Content-Length', compressed.length);
57
- originalEnd.call(res, compressed);
58
- } else {
59
- originalEnd.call(res, buffer);
60
- }
61
- });
62
- } else {
63
- originalEnd.call(res, buffer);
64
- }
65
- };
66
-
67
- await next();
68
- };
69
- };
70
-
71
- // Rate Limiting Middleware
72
- export const rateLimitMiddleware = (config) => {
73
- const requestCounts = new Map();
74
-
75
- return async (req, res, next) => {
76
- const clientId = req.socket.remoteAddress;
77
- const now = Date.now();
78
- const windowStart = now - config.windowMs;
79
-
80
- if (!requestCounts.has(clientId)) {
81
- requestCounts.set(clientId, []);
82
- }
83
-
84
- const requests = requestCounts.get(clientId);
85
- const recentRequests = requests.filter(time => time > windowStart);
86
-
87
- if (recentRequests.length >= config.maxRequests) {
88
- res.writeHead(429, { 'Content-Type': 'text/plain' });
89
- res.end(config.message);
90
- return;
91
- }
92
-
93
- recentRequests.push(now);
94
- requestCounts.set(clientId, recentRequests);
95
-
96
- await next();
97
- };
98
- };
99
-
100
- // Security Headers Middleware
101
- export const securityMiddleware = (config) => {
102
- return async (req, res, next) => {
103
- for (const [header, value] of Object.entries(config.headers)) {
104
- res.setHeader(header, value);
105
- }
106
- await next();
107
- };
108
- };
109
-
110
- // Logging Middleware
111
- export const loggingMiddleware = (config, log) => {
112
- return async (req, res, next) => {
113
- const startTime = Date.now();
114
- const userAgent = config.includeUserAgent ? req.headers['user-agent'] : '';
115
-
116
- // Store original end to capture response
117
- const originalEnd = res.end;
118
- res.end = function(...args) {
119
- const responseTime = Date.now() - startTime;
120
- let logMessage = `${req.method} ${req.url}`;
121
-
122
- if (config.includeResponseTime) {
123
- logMessage += ` - ${responseTime}ms`;
124
- }
125
-
126
- if (config.includeUserAgent && userAgent) {
127
- logMessage += ` - ${userAgent}`;
128
- }
129
-
130
- log(logMessage, 1);
131
- originalEnd.apply(res, args);
132
- };
133
-
134
- await next();
135
- };
136
- };
1
+ // Built-in middleware functions for Kempo Server
2
+ import zlib from 'zlib';
3
+
4
+ // CORS Middleware
5
+ export const corsMiddleware = (config) => {
6
+ return async (req, res, next) => {
7
+ const origin = req.headers.origin;
8
+ const allowedOrigins = Array.isArray(config.origin) ? config.origin : [config.origin];
9
+
10
+ if (config.origin === '*' || allowedOrigins.includes(origin)) {
11
+ res.setHeader('Access-Control-Allow-Origin', origin || '*');
12
+ }
13
+
14
+ res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
15
+ res.setHeader('Access-Control-Allow-Headers', config.headers.join(', '));
16
+
17
+ // Handle preflight requests
18
+ if (req.method === 'OPTIONS') {
19
+ res.writeHead(200);
20
+ res.end();
21
+ return;
22
+ }
23
+
24
+ await next();
25
+ };
26
+ };
27
+
28
+ // Compression Middleware
29
+ export const compressionMiddleware = (config) => {
30
+ return async (req, res, next) => {
31
+ const acceptEncoding = req.headers['accept-encoding'] || '';
32
+
33
+ if (!acceptEncoding.includes('gzip')) {
34
+ return await next();
35
+ }
36
+
37
+ const originalEnd = res.end;
38
+ const originalWrite = res.write;
39
+ const chunks = [];
40
+
41
+ res.write = function(chunk) {
42
+ if (chunk) chunks.push(Buffer.from(chunk));
43
+ return true;
44
+ };
45
+
46
+ res.end = function(chunk) {
47
+ if (chunk) chunks.push(Buffer.from(chunk));
48
+
49
+ const buffer = Buffer.concat(chunks);
50
+
51
+ // Only compress if above threshold
52
+ if (buffer.length >= config.threshold) {
53
+ zlib.gzip(buffer, (err, compressed) => {
54
+ if (!err && compressed.length < buffer.length) {
55
+ res.setHeader('Content-Encoding', 'gzip');
56
+ res.setHeader('Content-Length', compressed.length);
57
+ originalEnd.call(res, compressed);
58
+ } else {
59
+ originalEnd.call(res, buffer);
60
+ }
61
+ });
62
+ } else {
63
+ originalEnd.call(res, buffer);
64
+ }
65
+ };
66
+
67
+ await next();
68
+ };
69
+ };
70
+
71
+ // Rate Limiting Middleware
72
+ export const rateLimitMiddleware = (config) => {
73
+ const requestCounts = new Map();
74
+
75
+ return async (req, res, next) => {
76
+ const clientId = req.socket.remoteAddress;
77
+ const now = Date.now();
78
+ const windowStart = now - config.windowMs;
79
+
80
+ if (!requestCounts.has(clientId)) {
81
+ requestCounts.set(clientId, []);
82
+ }
83
+
84
+ const requests = requestCounts.get(clientId);
85
+ const recentRequests = requests.filter(time => time > windowStart);
86
+
87
+ if (recentRequests.length >= config.maxRequests) {
88
+ res.writeHead(429, { 'Content-Type': 'text/plain' });
89
+ res.end(config.message);
90
+ return;
91
+ }
92
+
93
+ recentRequests.push(now);
94
+ requestCounts.set(clientId, recentRequests);
95
+
96
+ await next();
97
+ };
98
+ };
99
+
100
+ // Security Headers Middleware
101
+ export const securityMiddleware = (config) => {
102
+ return async (req, res, next) => {
103
+ for (const [header, value] of Object.entries(config.headers)) {
104
+ res.setHeader(header, value);
105
+ }
106
+ await next();
107
+ };
108
+ };
109
+
110
+ // Logging Middleware
111
+ export const loggingMiddleware = (config, log) => {
112
+ return async (req, res, next) => {
113
+ const startTime = Date.now();
114
+ const userAgent = config.includeUserAgent ? req.headers['user-agent'] : '';
115
+
116
+ // Store original end to capture response
117
+ const originalEnd = res.end;
118
+ res.end = function(...args) {
119
+ const responseTime = Date.now() - startTime;
120
+ let logMessage = `${req.method} ${req.url}`;
121
+
122
+ if (config.includeResponseTime) {
123
+ logMessage += ` - ${responseTime}ms`;
124
+ }
125
+
126
+ if (config.includeUserAgent && userAgent) {
127
+ logMessage += ` - ${userAgent}`;
128
+ }
129
+
130
+ log(logMessage, 1);
131
+ originalEnd.apply(res, args);
132
+ };
133
+
134
+ await next();
135
+ };
136
+ };
package/defaultConfig.js CHANGED
@@ -1,130 +1,130 @@
1
- export default {
2
- allowedMimes: {
3
- html: "text/html",
4
- htm: "text/html",
5
- shtml: "text/html",
6
- css: "text/css",
7
- xml: "text/xml",
8
- gif: "image/gif",
9
- jpeg: "image/jpeg",
10
- jpg: "image/jpeg",
11
- js: "application/javascript",
12
- mjs: "application/javascript",
13
- json: "application/json",
14
- webp: "image/webp",
15
- png: "image/png",
16
- svg: "image/svg+xml",
17
- svgz: "image/svg+xml",
18
- ico: "image/x-icon",
19
- webm: "video/webm",
20
- mp4: "video/mp4",
21
- m4v: "video/mp4",
22
- ogv: "video/ogg",
23
- mp3: "audio/mpeg",
24
- ogg: "audio/ogg",
25
- wav: "audio/wav",
26
- woff: "font/woff",
27
- woff2: "font/woff2",
28
- ttf: "font/ttf",
29
- otf: "font/otf",
30
- eot: "application/vnd.ms-fontobject",
31
- pdf: "application/pdf",
32
- txt: "text/plain",
33
- webmanifest: "application/manifest+json",
34
- md: "text/markdown",
35
- csv: "text/csv",
36
- doc: "application/msword",
37
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
38
- xls: "application/vnd.ms-excel",
39
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
40
- ppt: "application/vnd.ms-powerpoint",
41
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
42
- avif: "image/avif",
43
- wasm: "application/wasm"
44
- },
45
- disallowedRegex: [
46
- "^/\\..*",
47
- "\\.config$",
48
- "\\.env$",
49
- "\\.git/",
50
- "\\.htaccess$",
51
- "\\.htpasswd$",
52
- "^/node_modules/",
53
- "^/vendor/",
54
- "\\.log$",
55
- "\\.bak$",
56
- "\\.sql$",
57
- "\\.ini$",
58
- "password",
59
- "config\\.php$",
60
- "wp-config\\.php$",
61
- "\\.DS_Store$"
62
- ],
63
- routeFiles: [
64
- 'GET.js',
65
- 'POST.js',
66
- 'PUT.js',
67
- 'DELETE.js',
68
- 'HEAD.js',
69
- 'OPTIONS.js',
70
- 'PATCH.js',
71
- 'CONNECT.js',
72
- 'TRACE.js',
73
- 'index.js'
74
- ],
75
- noRescanPaths: [
76
- "^\\.well-known/",
77
- "/favicon\\.ico$",
78
- "/robots\\.txt$",
79
- "/sitemap\\.xml$",
80
- "/apple-touch-icon",
81
- "/android-chrome-",
82
- "/browserconfig\\.xml$",
83
- "/manifest\\.json$",
84
- "\\.map$",
85
- "/__webpack_hmr$",
86
- "/hot-update\\.",
87
- "/sockjs-node/",
88
- ],
89
- maxRescanAttempts: 3,
90
- customRoutes: {
91
- // Example: "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
92
- // Wildcard example: "kempo/*": "./node_modules/kempo/dust/*"
93
- },
94
- middleware: {
95
- // Built-in middleware configuration
96
- cors: {
97
- enabled: false,
98
- origin: "*",
99
- methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
100
- headers: ["Content-Type", "Authorization"]
101
- },
102
- compression: {
103
- enabled: false,
104
- threshold: 1024 // Only compress files larger than 1KB
105
- },
106
- rateLimit: {
107
- enabled: false,
108
- maxRequests: 100,
109
- windowMs: 60000, // 1 minute
110
- message: "Too many requests"
111
- },
112
- security: {
113
- enabled: true,
114
- headers: {
115
- "X-Content-Type-Options": "nosniff",
116
- "X-Frame-Options": "DENY",
117
- "X-XSS-Protection": "1; mode=block"
118
- }
119
- },
120
- logging: {
121
- enabled: true,
122
- includeUserAgent: false,
123
- includeResponseTime: true
124
- },
125
- // Custom middleware files
126
- custom: [
127
- // Example: "./middleware/auth.js"
128
- ]
129
- }
1
+ export default {
2
+ allowedMimes: {
3
+ html: "text/html",
4
+ htm: "text/html",
5
+ shtml: "text/html",
6
+ css: "text/css",
7
+ xml: "text/xml",
8
+ gif: "image/gif",
9
+ jpeg: "image/jpeg",
10
+ jpg: "image/jpeg",
11
+ js: "application/javascript",
12
+ mjs: "application/javascript",
13
+ json: "application/json",
14
+ webp: "image/webp",
15
+ png: "image/png",
16
+ svg: "image/svg+xml",
17
+ svgz: "image/svg+xml",
18
+ ico: "image/x-icon",
19
+ webm: "video/webm",
20
+ mp4: "video/mp4",
21
+ m4v: "video/mp4",
22
+ ogv: "video/ogg",
23
+ mp3: "audio/mpeg",
24
+ ogg: "audio/ogg",
25
+ wav: "audio/wav",
26
+ woff: "font/woff",
27
+ woff2: "font/woff2",
28
+ ttf: "font/ttf",
29
+ otf: "font/otf",
30
+ eot: "application/vnd.ms-fontobject",
31
+ pdf: "application/pdf",
32
+ txt: "text/plain",
33
+ webmanifest: "application/manifest+json",
34
+ md: "text/markdown",
35
+ csv: "text/csv",
36
+ doc: "application/msword",
37
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
38
+ xls: "application/vnd.ms-excel",
39
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
40
+ ppt: "application/vnd.ms-powerpoint",
41
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
42
+ avif: "image/avif",
43
+ wasm: "application/wasm"
44
+ },
45
+ disallowedRegex: [
46
+ "^/\\..*",
47
+ "\\.config$",
48
+ "\\.env$",
49
+ "\\.git/",
50
+ "\\.htaccess$",
51
+ "\\.htpasswd$",
52
+ "^/node_modules/",
53
+ "^/vendor/",
54
+ "\\.log$",
55
+ "\\.bak$",
56
+ "\\.sql$",
57
+ "\\.ini$",
58
+ "password",
59
+ "config\\.php$",
60
+ "wp-config\\.php$",
61
+ "\\.DS_Store$"
62
+ ],
63
+ routeFiles: [
64
+ 'GET.js',
65
+ 'POST.js',
66
+ 'PUT.js',
67
+ 'DELETE.js',
68
+ 'HEAD.js',
69
+ 'OPTIONS.js',
70
+ 'PATCH.js',
71
+ 'CONNECT.js',
72
+ 'TRACE.js',
73
+ 'index.js'
74
+ ],
75
+ noRescanPaths: [
76
+ "^\\.well-known/",
77
+ "/favicon\\.ico$",
78
+ "/robots\\.txt$",
79
+ "/sitemap\\.xml$",
80
+ "/apple-touch-icon",
81
+ "/android-chrome-",
82
+ "/browserconfig\\.xml$",
83
+ "/manifest\\.json$",
84
+ "\\.map$",
85
+ "/__webpack_hmr$",
86
+ "/hot-update\\.",
87
+ "/sockjs-node/",
88
+ ],
89
+ maxRescanAttempts: 3,
90
+ customRoutes: {
91
+ // Example: "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css"
92
+ // Wildcard example: "kempo/*": "./node_modules/kempo/dust/*"
93
+ },
94
+ middleware: {
95
+ // Built-in middleware configuration
96
+ cors: {
97
+ enabled: false,
98
+ origin: "*",
99
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
100
+ headers: ["Content-Type", "Authorization"]
101
+ },
102
+ compression: {
103
+ enabled: false,
104
+ threshold: 1024 // Only compress files larger than 1KB
105
+ },
106
+ rateLimit: {
107
+ enabled: false,
108
+ maxRequests: 100,
109
+ windowMs: 60000, // 1 minute
110
+ message: "Too many requests"
111
+ },
112
+ security: {
113
+ enabled: true,
114
+ headers: {
115
+ "X-Content-Type-Options": "nosniff",
116
+ "X-Frame-Options": "DENY",
117
+ "X-XSS-Protection": "1; mode=block"
118
+ }
119
+ },
120
+ logging: {
121
+ enabled: true,
122
+ includeUserAgent: false,
123
+ includeResponseTime: true
124
+ },
125
+ // Custom middleware files
126
+ custom: [
127
+ // Example: "./middleware/auth.js"
128
+ ]
129
+ }
130
130
  }
package/docs/.config.json CHANGED
@@ -1,6 +1,6 @@
1
- {
2
- "customRoutes": {
3
- "/essential.css": "./node_modules/essentialcss/dist/essential.min.css",
4
- "/essential-hljs.css": "./node_modules/essentialcss/dist/essential-hljs.min.css"
5
- }
1
+ {
2
+ "customRoutes": {
3
+ "/essential.css": "./node_modules/essentialcss/dist/essential.min.css",
4
+ "/essential-hljs.css": "./node_modules/essentialcss/dist/essential-hljs.min.css"
5
+ }
6
6
  }
@@ -1,19 +1,19 @@
1
- {
2
- "customRoutes": {
3
- "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css",
4
- "/vendor/bootstrap.js": "./node_modules/bootstrap/dist/js/bootstrap.min.js",
5
- "/vendor/jquery.js": "./node_modules/jquery/dist/jquery.min.js"
6
- },
7
- "allowedMimes": {
8
- "woff": "font/woff",
9
- "woff2": "font/woff2"
10
- },
11
- "disallowedRegex": [
12
- "private/",
13
- "\\.env$"
14
- ],
15
- "noRescanPaths": [
16
- "/vendor/"
17
- ],
18
- "maxRescanAttempts": 3
19
- }
1
+ {
2
+ "customRoutes": {
3
+ "/vendor/bootstrap.css": "./node_modules/bootstrap/dist/css/bootstrap.min.css",
4
+ "/vendor/bootstrap.js": "./node_modules/bootstrap/dist/js/bootstrap.min.js",
5
+ "/vendor/jquery.js": "./node_modules/jquery/dist/jquery.min.js"
6
+ },
7
+ "allowedMimes": {
8
+ "woff": "font/woff",
9
+ "woff2": "font/woff2"
10
+ },
11
+ "disallowedRegex": [
12
+ "private/",
13
+ "\\.env$"
14
+ ],
15
+ "noRescanPaths": [
16
+ "/vendor/"
17
+ ],
18
+ "maxRescanAttempts": 3
19
+ }
@@ -1,15 +1,15 @@
1
- export default async function(request, response) {
2
- const { id } = request.params;
3
-
4
- // Example user data
5
- const userData = {
6
- id: id,
7
- profile: {
8
- name: `${id.charAt(0).toUpperCase()}${id.slice(1)}`,
9
- joinDate: '2024-01-15',
10
- posts: 42
11
- }
12
- };
13
-
14
- response.json(userData);
15
- }
1
+ export default async function(request, response) {
2
+ const { id } = request.params;
3
+
4
+ // Example user data
5
+ const userData = {
6
+ id: id,
7
+ profile: {
8
+ name: `${id.charAt(0).toUpperCase()}${id.slice(1)}`,
9
+ joinDate: '2024-01-15',
10
+ posts: 42
11
+ }
12
+ };
13
+
14
+ response.json(userData);
15
+ }
@@ -1,12 +1,12 @@
1
- export default async function(request, response) {
2
- const { id, info } = request.params;
3
-
4
- // Example response for deleting user info
5
- const result = {
6
- id: id,
7
- message: 'User info deleted successfully',
8
- deletedAt: new Date().toISOString()
9
- };
10
-
11
- response.json(result);
12
- }
1
+ export default async function(request, response) {
2
+ const { id, info } = request.params;
3
+
4
+ // Example response for deleting user info
5
+ const result = {
6
+ id: id,
7
+ message: 'User info deleted successfully',
8
+ deletedAt: new Date().toISOString()
9
+ };
10
+
11
+ response.json(result);
12
+ }
@@ -1,17 +1,17 @@
1
- export default async function(request, response) {
2
- const { id, info } = request.params;
3
-
4
- // Example detailed user info
5
- const userInfo = {
6
- id: id,
7
- details: {
8
- bio: `This is ${id}'s bio`,
9
- location: 'Earth',
10
- website: `https://${id}.dev`,
11
- followers: 123,
12
- following: 456
13
- }
14
- };
15
-
16
- response.json(userInfo);
17
- }
1
+ export default async function(request, response) {
2
+ const { id, info } = request.params;
3
+
4
+ // Example detailed user info
5
+ const userInfo = {
6
+ id: id,
7
+ details: {
8
+ bio: `This is ${id}'s bio`,
9
+ location: 'Earth',
10
+ website: `https://${id}.dev`,
11
+ followers: 123,
12
+ following: 456
13
+ }
14
+ };
15
+
16
+ response.json(userInfo);
17
+ }