nibula 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,267 +1,267 @@
1
- 'use strict';
2
-
3
- /**
4
- * Mirror of src/backend/_core/index.php (the front controller).
5
- *
6
- * PHP was invoked once per request by the web server through PHP-FPM. Node is
7
- * the server itself, so this file boots an HTTP listener and runs the exact same
8
- * pipeline for every request:
9
- *
10
- * 1. request parsing (strip /api prefix, split segments, reject . / ..)
11
- * 2. endpoint resolution (dynamic routing with subdirectories + route params)
12
- * 3. headers + CORS
13
- * 4. rate limiting
14
- * 5. authentication guard for protected endpoints
15
- * 6. dispatch to the matched endpoint file
16
- *
17
- * Endpoints keep the same authoring style as PHP: a file that receives
18
- * { method, requestParams, Response, ... } and calls Response.success/error.
19
- */
20
-
21
- const http = require('http');
22
- const fs = require('fs');
23
- const path = require('path');
24
- const crypto = require('crypto');
25
-
26
- const { createResponse, HALT } = require('./modules/Response');
27
- const RateLimiter = require('./modules/RateLimiter');
28
- const { loadConfig, handleException } = require('./init');
29
-
30
- const CORE_PATH = __dirname;
31
-
32
- // Load configuration once (init.php loaded it per invocation; here per process).
33
- const config = loadConfig();
34
-
35
- // Environment: mirror the display_errors toggle by picking the debug flag.
36
- const APP_ENV = config.APP_ENV || 'production';
37
-
38
- // Where to look for the 404.html generated by the static build.
39
- // In a Nibula build the backend lives at out/backend/, so the site root (out/)
40
- // is two levels up from _core. We default to that, so 404.html is found without
41
- // any extra configuration; override with the DOCUMENT_ROOT env var if needed.
42
- // (equivalent of $_SERVER['DOCUMENT_ROOT'] in the PHP front controller).
43
- const DOCUMENT_ROOT = process.env.DOCUMENT_ROOT || path.join(CORE_PATH, '..', '..');
44
-
45
- const basePublic = path.join(CORE_PATH, '..', 'api', 'public');
46
- const baseProtected = path.join(CORE_PATH, '..', 'api', 'protected');
47
-
48
- /**
49
- * hash_equals equivalent: constant-time string comparison, safe on unequal
50
- * lengths (timingSafeEqual throws if buffers differ in length).
51
- */
52
- function hashEquals(known, given) {
53
- const a = Buffer.from(String(known));
54
- const b = Buffer.from(String(given));
55
- if (a.length !== b.length) {
56
- return false;
57
- }
58
- return crypto.timingSafeEqual(a, b);
59
- }
60
-
61
- /** Serve the static 404 page if available, else a plain fallback. */
62
- function send404(res) {
63
- res.statusCode = 404;
64
- const errorPage = DOCUMENT_ROOT ? path.join(DOCUMENT_ROOT, '404.html') : '';
65
- if (errorPage && fs.existsSync(errorPage)) {
66
- res.setHeader('Content-Type', 'text/html; charset=UTF-8');
67
- res.end(fs.readFileSync(errorPage));
68
- } else {
69
- res.setHeader('Content-Type', 'text/html; charset=UTF-8');
70
- res.end('<h1>404 Not Found</h1>The requested URL was not found on this server.');
71
- }
72
- }
73
-
74
- /** Read and JSON-parse the request body (available to endpoints as ctx.body). */
75
- function readBody(req) {
76
- return new Promise((resolve) => {
77
- const chunks = [];
78
- req.on('data', (c) => chunks.push(c));
79
- req.on('end', () => {
80
- const raw = Buffer.concat(chunks).toString('utf8');
81
- if (!raw) return resolve({ raw: '', json: null });
82
- let json = null;
83
- try { json = JSON.parse(raw); } catch (e) { json = null; }
84
- resolve({ raw, json });
85
- });
86
- req.on('error', () => resolve({ raw: '', json: null }));
87
- });
88
- }
89
-
90
- async function handleRequest(req, res) {
91
- const Response = createResponse(res);
92
-
93
- try {
94
- // =====================================================
95
- // 1. REQUEST PARSING
96
- // =====================================================
97
- const method = req.method;
98
- const parsedUrl = new URL(req.url, 'http://localhost');
99
- let uri = parsedUrl.pathname;
100
-
101
- // Strip a leading /api, then trim trailing slash (rtrim(..., '/') ?: '/').
102
- uri = uri.replace(/^\/api/, '');
103
- uri = uri.replace(/\/+$/, '');
104
- if (uri === '') uri = '/';
105
-
106
- const parts = uri.split('/').filter((p) => p !== '');
107
-
108
- for (const part of parts) {
109
- if (part === '..' || part === '.') {
110
- Response.error('Bad Request', 400);
111
- }
112
- }
113
-
114
- // =====================================================
115
- // 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
116
- // =====================================================
117
- let endpointFile = null;
118
- let isProtected = false;
119
-
120
- const checkParts = parts.slice();
121
- const params = [];
122
-
123
- while (checkParts.length > 0) {
124
- const relativePath = checkParts.join('/') + '.js';
125
-
126
- const publicCandidate = path.join(basePublic, relativePath);
127
- if (fs.existsSync(publicCandidate)) {
128
- endpointFile = publicCandidate;
129
- isProtected = false;
130
- break;
131
- }
132
-
133
- const protectedCandidate = path.join(baseProtected, relativePath);
134
- if (fs.existsSync(protectedCandidate)) {
135
- endpointFile = protectedCandidate;
136
- isProtected = true;
137
- break;
138
- }
139
-
140
- // No match: peel the last segment as a route param and retry.
141
- params.unshift(checkParts.pop());
142
- }
143
-
144
- // No endpoint → HTML 404 page.
145
- if (!endpointFile) {
146
- send404(res);
147
- return;
148
- }
149
-
150
- // Compute the endpoint path (relative, without extension) used as the key
151
- // for CUSTOM_ENDPOINT_KEYS / CUSTOM_ENDPOINT_ORIGINS.
152
- const base = isProtected ? baseProtected : basePublic;
153
- let endpointPath = endpointFile
154
- .split(path.sep).join('/')
155
- .replace(base.split(path.sep).join('/'), '')
156
- .replace(/\.js$/, '')
157
- .replace(/^\//, '');
158
-
159
- // =====================================================
160
- // 3. HEADERS AND CORS (only if the endpoint exists)
161
- // =====================================================
162
- res.setHeader('Content-Type', 'application/json; charset=UTF-8');
163
- res.setHeader('X-Content-Type-Options', 'nosniff');
164
- res.setHeader('X-Frame-Options', 'DENY');
165
- res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
166
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
167
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key');
168
-
169
- const originsForEndpoint =
170
- (config.CUSTOM_ENDPOINT_ORIGINS && config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
171
- config.GENERAL_ALLOWED_ORIGINS ||
172
- [];
173
-
174
- const origin = req.headers['origin'] || '';
175
-
176
- if (origin !== '' && originsForEndpoint.includes(origin)) {
177
- res.setHeader('Access-Control-Allow-Origin', origin);
178
- res.setHeader('Vary', 'Origin');
179
- } else if (originsForEndpoint.includes('*')) {
180
- res.setHeader('Access-Control-Allow-Origin', '*');
181
- }
182
-
183
- if (method === 'OPTIONS') {
184
- res.statusCode = 204;
185
- res.end();
186
- return;
187
- }
188
-
189
- // Rate limit: 60 requests / 60 seconds per IP (same defaults as PHP).
190
- const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
191
- || req.socket.remoteAddress
192
- || '';
193
- RateLimiter.check(ip, 60, 60, Response, res);
194
-
195
- // =====================================================
196
- // 4. AUTHENTICATION GUARD
197
- // =====================================================
198
- if (isProtected) {
199
- const validKey =
200
- (config.CUSTOM_ENDPOINT_KEYS && config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
201
- config.GENERAL_API_KEY ||
202
- '';
203
- const apiKey = req.headers['x-api-key'] || '';
204
-
205
- if (validKey === '' || !hashEquals(validKey, apiKey)) {
206
- Response.error('Unauthorized. X_API_KEY is incorrect or missing', 401);
207
- }
208
- }
209
-
210
- // =====================================================
211
- // 5. DISPATCH
212
- // =====================================================
213
- const body = await readBody(req);
214
-
215
- // eslint-disable-next-line import/no-dynamic-require, global-require
216
- const handler = require(endpointFile);
217
-
218
- const context = {
219
- method, // HTTP method
220
- requestParams: params, // leftover URL segments (route params)
221
- Response, // request-bound Response helper
222
- query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
223
- body: body.json, // parsed JSON body (or null)
224
- rawBody: body.raw, // raw body string
225
- headers: req.headers,
226
- req,
227
- res,
228
- config,
229
- CORE_PATH,
230
- };
231
-
232
- await handler(context);
233
-
234
- // If the endpoint returned without sending a response, mirror PHP's
235
- // implicit end of script: nothing more to do. Close if still open.
236
- if (!res.writableEnded) {
237
- res.end();
238
- }
239
- } catch (err) {
240
- if (err === HALT) {
241
- // A response was already sent; unwind quietly (≈ PHP exit).
242
- return;
243
- }
244
- // set_exception_handler equivalent: any real error → 500.
245
- try {
246
- handleException(err, config, Response);
247
- } catch (inner) {
248
- if (inner === HALT) return;
249
- if (!res.writableEnded) {
250
- res.statusCode = 500;
251
- res.end(JSON.stringify({ status: 'error', message: 'Internal server error', code: 500 }));
252
- }
253
- }
254
- }
255
- }
256
-
257
- const HOST = process.env.HOST || '127.0.0.1';
258
- const PORT = parseInt(process.env.PORT || '3000', 10);
259
-
260
- const server = http.createServer(handleRequest);
261
-
262
- server.listen(PORT, HOST, () => {
263
- // eslint-disable-next-line no-console
264
- console.log(`[backend-node] listening on http://${HOST}:${PORT} (env: ${APP_ENV})`);
265
- });
266
-
267
- module.exports = server;
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mirror of src/backend/_core/index.php (the front controller).
5
+ *
6
+ * PHP was invoked once per request by the web server through PHP-FPM. Node is
7
+ * the server itself, so this file boots an HTTP listener and runs the exact same
8
+ * pipeline for every request:
9
+ *
10
+ * 1. request parsing (strip /api prefix, split segments, reject . / ..)
11
+ * 2. endpoint resolution (dynamic routing with subdirectories + route params)
12
+ * 3. headers + CORS
13
+ * 4. rate limiting
14
+ * 5. authentication guard for protected endpoints
15
+ * 6. dispatch to the matched endpoint file
16
+ *
17
+ * Endpoints keep the same authoring style as PHP: a file that receives
18
+ * { method, requestParams, Response, ... } and calls Response.success/error.
19
+ */
20
+
21
+ const http = require('http');
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const crypto = require('crypto');
25
+
26
+ const { createResponse, HALT } = require('./modules/Response');
27
+ const RateLimiter = require('./modules/RateLimiter');
28
+ const { loadConfig, handleException } = require('./init');
29
+
30
+ const CORE_PATH = __dirname;
31
+
32
+ // Load configuration once (init.php loaded it per invocation; here per process).
33
+ const config = loadConfig();
34
+
35
+ // Environment: mirror the display_errors toggle by picking the debug flag.
36
+ const APP_ENV = config.APP_ENV || 'production';
37
+
38
+ // Where to look for the 404.html generated by the static build.
39
+ // In a Nibula build the backend lives at out/backend/, so the site root (out/)
40
+ // is two levels up from _core. We default to that, so 404.html is found without
41
+ // any extra configuration; override with the DOCUMENT_ROOT env var if needed.
42
+ // (equivalent of $_SERVER['DOCUMENT_ROOT'] in the PHP front controller).
43
+ const DOCUMENT_ROOT = process.env.DOCUMENT_ROOT || path.join(CORE_PATH, '..', '..');
44
+
45
+ const basePublic = path.join(CORE_PATH, '..', 'api', 'public');
46
+ const baseProtected = path.join(CORE_PATH, '..', 'api', 'protected');
47
+
48
+ /**
49
+ * hash_equals equivalent: constant-time string comparison, safe on unequal
50
+ * lengths (timingSafeEqual throws if buffers differ in length).
51
+ */
52
+ function hashEquals(known, given) {
53
+ const a = Buffer.from(String(known));
54
+ const b = Buffer.from(String(given));
55
+ if (a.length !== b.length) {
56
+ return false;
57
+ }
58
+ return crypto.timingSafeEqual(a, b);
59
+ }
60
+
61
+ /** Serve the static 404 page if available, else a plain fallback. */
62
+ function send404(res) {
63
+ res.statusCode = 404;
64
+ const errorPage = DOCUMENT_ROOT ? path.join(DOCUMENT_ROOT, '404.html') : '';
65
+ if (errorPage && fs.existsSync(errorPage)) {
66
+ res.setHeader('Content-Type', 'text/html; charset=UTF-8');
67
+ res.end(fs.readFileSync(errorPage));
68
+ } else {
69
+ res.setHeader('Content-Type', 'text/html; charset=UTF-8');
70
+ res.end('<h1>404 Not Found</h1>The requested URL was not found on this server.');
71
+ }
72
+ }
73
+
74
+ /** Read and JSON-parse the request body (available to endpoints as ctx.body). */
75
+ function readBody(req) {
76
+ return new Promise((resolve) => {
77
+ const chunks = [];
78
+ req.on('data', (c) => chunks.push(c));
79
+ req.on('end', () => {
80
+ const raw = Buffer.concat(chunks).toString('utf8');
81
+ if (!raw) return resolve({ raw: '', json: null });
82
+ let json = null;
83
+ try { json = JSON.parse(raw); } catch (e) { json = null; }
84
+ resolve({ raw, json });
85
+ });
86
+ req.on('error', () => resolve({ raw: '', json: null }));
87
+ });
88
+ }
89
+
90
+ async function handleRequest(req, res) {
91
+ const Response = createResponse(res);
92
+
93
+ try {
94
+ // =====================================================
95
+ // 1. REQUEST PARSING
96
+ // =====================================================
97
+ const method = req.method;
98
+ const parsedUrl = new URL(req.url, 'http://localhost');
99
+ let uri = parsedUrl.pathname;
100
+
101
+ // Strip a leading /api, then trim trailing slash (rtrim(..., '/') ?: '/').
102
+ uri = uri.replace(/^\/api/, '');
103
+ uri = uri.replace(/\/+$/, '');
104
+ if (uri === '') uri = '/';
105
+
106
+ const parts = uri.split('/').filter((p) => p !== '');
107
+
108
+ for (const part of parts) {
109
+ if (part === '..' || part === '.') {
110
+ Response.error('Bad Request', 400);
111
+ }
112
+ }
113
+
114
+ // =====================================================
115
+ // 2. ENDPOINT RESOLUTION (ROUTING WITH SUBDIRECTORIES)
116
+ // =====================================================
117
+ let endpointFile = null;
118
+ let isProtected = false;
119
+
120
+ const checkParts = parts.slice();
121
+ const params = [];
122
+
123
+ while (checkParts.length > 0) {
124
+ const relativePath = checkParts.join('/') + '.js';
125
+
126
+ const publicCandidate = path.join(basePublic, relativePath);
127
+ if (fs.existsSync(publicCandidate)) {
128
+ endpointFile = publicCandidate;
129
+ isProtected = false;
130
+ break;
131
+ }
132
+
133
+ const protectedCandidate = path.join(baseProtected, relativePath);
134
+ if (fs.existsSync(protectedCandidate)) {
135
+ endpointFile = protectedCandidate;
136
+ isProtected = true;
137
+ break;
138
+ }
139
+
140
+ // No match: peel the last segment as a route param and retry.
141
+ params.unshift(checkParts.pop());
142
+ }
143
+
144
+ // No endpoint → HTML 404 page.
145
+ if (!endpointFile) {
146
+ send404(res);
147
+ return;
148
+ }
149
+
150
+ // Compute the endpoint path (relative, without extension) used as the key
151
+ // for CUSTOM_ENDPOINT_KEYS / CUSTOM_ENDPOINT_ORIGINS.
152
+ const base = isProtected ? baseProtected : basePublic;
153
+ let endpointPath = endpointFile
154
+ .split(path.sep).join('/')
155
+ .replace(base.split(path.sep).join('/'), '')
156
+ .replace(/\.js$/, '')
157
+ .replace(/^\//, '');
158
+
159
+ // =====================================================
160
+ // 3. HEADERS AND CORS (only if the endpoint exists)
161
+ // =====================================================
162
+ res.setHeader('Content-Type', 'application/json; charset=UTF-8');
163
+ res.setHeader('X-Content-Type-Options', 'nosniff');
164
+ res.setHeader('X-Frame-Options', 'DENY');
165
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
166
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
167
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Api-Key');
168
+
169
+ const originsForEndpoint =
170
+ (config.CUSTOM_ENDPOINT_ORIGINS && config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
171
+ config.GENERAL_ALLOWED_ORIGINS ||
172
+ [];
173
+
174
+ const origin = req.headers['origin'] || '';
175
+
176
+ if (origin !== '' && originsForEndpoint.includes(origin)) {
177
+ res.setHeader('Access-Control-Allow-Origin', origin);
178
+ res.setHeader('Vary', 'Origin');
179
+ } else if (originsForEndpoint.includes('*')) {
180
+ res.setHeader('Access-Control-Allow-Origin', '*');
181
+ }
182
+
183
+ if (method === 'OPTIONS') {
184
+ res.statusCode = 204;
185
+ res.end();
186
+ return;
187
+ }
188
+
189
+ // Rate limit: 60 requests / 60 seconds per IP (same defaults as PHP).
190
+ const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim()
191
+ || req.socket.remoteAddress
192
+ || '';
193
+ RateLimiter.check(ip, 60, 60, Response, res);
194
+
195
+ // =====================================================
196
+ // 4. AUTHENTICATION GUARD
197
+ // =====================================================
198
+ if (isProtected) {
199
+ const validKey =
200
+ (config.CUSTOM_ENDPOINT_KEYS && config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
201
+ config.GENERAL_API_KEY ||
202
+ '';
203
+ const apiKey = req.headers['x-api-key'] || '';
204
+
205
+ if (validKey === '' || !hashEquals(validKey, apiKey)) {
206
+ Response.error('Unauthorized. X_API_KEY is incorrect or missing', 401);
207
+ }
208
+ }
209
+
210
+ // =====================================================
211
+ // 5. DISPATCH
212
+ // =====================================================
213
+ const body = await readBody(req);
214
+
215
+ // eslint-disable-next-line import/no-dynamic-require, global-require
216
+ const handler = require(endpointFile);
217
+
218
+ const context = {
219
+ method, // HTTP method
220
+ requestParams: params, // leftover URL segments (route params)
221
+ Response, // request-bound Response helper
222
+ query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
223
+ body: body.json, // parsed JSON body (or null)
224
+ rawBody: body.raw, // raw body string
225
+ headers: req.headers,
226
+ req,
227
+ res,
228
+ config,
229
+ CORE_PATH,
230
+ };
231
+
232
+ await handler(context);
233
+
234
+ // If the endpoint returned without sending a response, mirror PHP's
235
+ // implicit end of script: nothing more to do. Close if still open.
236
+ if (!res.writableEnded) {
237
+ res.end();
238
+ }
239
+ } catch (err) {
240
+ if (err === HALT) {
241
+ // A response was already sent; unwind quietly (≈ PHP exit).
242
+ return;
243
+ }
244
+ // set_exception_handler equivalent: any real error → 500.
245
+ try {
246
+ handleException(err, config, Response);
247
+ } catch (inner) {
248
+ if (inner === HALT) return;
249
+ if (!res.writableEnded) {
250
+ res.statusCode = 500;
251
+ res.end(JSON.stringify({ status: 'error', message: 'Internal server error', code: 500 }));
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ const HOST = process.env.HOST || '127.0.0.1';
258
+ const PORT = parseInt(process.env.PORT || '3000', 10);
259
+
260
+ const server = http.createServer(handleRequest);
261
+
262
+ server.listen(PORT, HOST, () => {
263
+ // eslint-disable-next-line no-console
264
+ console.log(`[backend-node] listening on http://${HOST}:${PORT} (env: ${APP_ENV})`);
265
+ });
266
+
267
+ module.exports = server;
@@ -1,52 +1,52 @@
1
- 'use strict';
2
-
3
- /**
4
- * Mirror of src/backend/_core/init.php
5
- *
6
- * init.php did three things:
7
- * 1. registered a global exception handler that turns any throwable into a
8
- * 500 JSON response (verbose in debug, generic in production);
9
- * 2. registered an error handler that promoted PHP warnings to exceptions;
10
- * 3. loaded config.php.
11
- *
12
- * In Node there is no per-request global handler, so the exception-to-500
13
- * translation lives in the front controller (index.js), which calls
14
- * handleException() below. Here we expose config loading and that helper.
15
- */
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
-
20
- /**
21
- * Load configuration. dirname(__dirname) in PHP pointed at the folder holding
22
- * config.php (the backend root). Here that is the parent of _core.
23
- * If config.js is missing (e.g. fresh clone) we fall back to example.config.js,
24
- * mirroring the "copy example.config to config" guidance.
25
- */
26
- function loadConfig() {
27
- const backendRoot = path.join(__dirname, '..');
28
- const configPath = path.join(backendRoot, 'config.js');
29
- const examplePath = path.join(backendRoot, 'example.config.js');
30
-
31
- if (fs.existsSync(configPath)) {
32
- return require(configPath);
33
- }
34
- return require(examplePath);
35
- }
36
-
37
- /**
38
- * Turn any thrown error into a 500 response, mirroring set_exception_handler.
39
- * @param {Error} exception
40
- * @param {object} config
41
- * @param {object} Response request-bound helper
42
- */
43
- function handleException(exception, config, Response) {
44
- const isDebug = (config.APP_ENV || 'production') !== 'production';
45
- Response.error(
46
- isDebug ? exception.message : 'Internal server error',
47
- 500,
48
- isDebug ? { stack: exception.stack } : null
49
- );
50
- }
51
-
52
- module.exports = { loadConfig, handleException };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Mirror of src/backend/_core/init.php
5
+ *
6
+ * init.php did three things:
7
+ * 1. registered a global exception handler that turns any throwable into a
8
+ * 500 JSON response (verbose in debug, generic in production);
9
+ * 2. registered an error handler that promoted PHP warnings to exceptions;
10
+ * 3. loaded config.php.
11
+ *
12
+ * In Node there is no per-request global handler, so the exception-to-500
13
+ * translation lives in the front controller (index.js), which calls
14
+ * handleException() below. Here we expose config loading and that helper.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ /**
21
+ * Load configuration. dirname(__dirname) in PHP pointed at the folder holding
22
+ * config.php (the backend root). Here that is the parent of _core.
23
+ * If config.js is missing (e.g. fresh clone) we fall back to example.config.js,
24
+ * mirroring the "copy example.config to config" guidance.
25
+ */
26
+ function loadConfig() {
27
+ const backendRoot = path.join(__dirname, '..');
28
+ const configPath = path.join(backendRoot, 'config.js');
29
+ const examplePath = path.join(backendRoot, 'example.config.js');
30
+
31
+ if (fs.existsSync(configPath)) {
32
+ return require(configPath);
33
+ }
34
+ return require(examplePath);
35
+ }
36
+
37
+ /**
38
+ * Turn any thrown error into a 500 response, mirroring set_exception_handler.
39
+ * @param {Error} exception
40
+ * @param {object} config
41
+ * @param {object} Response request-bound helper
42
+ */
43
+ function handleException(exception, config, Response) {
44
+ const isDebug = (config.APP_ENV || 'production') !== 'production';
45
+ Response.error(
46
+ isDebug ? exception.message : 'Internal server error',
47
+ 500,
48
+ isDebug ? { stack: exception.stack } : null
49
+ );
50
+ }
51
+
52
+ module.exports = { loadConfig, handleException };