nibula 2.5.0 → 3.0.1

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 (50) hide show
  1. package/.eleventy.js +0 -7
  2. package/CHANGELOG.md +53 -3
  3. package/bin/create.js +43 -45
  4. package/bin/nibula.js +2 -8
  5. package/nginx.conf +1 -1
  6. package/package.json +3 -7
  7. package/src/backend/.htaccess +2 -2
  8. package/src/backend/core/index.js +329 -0
  9. package/src/backend/core/index.php +207 -0
  10. package/src/backend/core/init.js +37 -0
  11. package/src/backend/core/init.php +54 -0
  12. package/src/backend/core/modules/RateLimiter.js +55 -0
  13. package/src/backend/core/modules/RateLimiter.php +39 -0
  14. package/src/backend/{_core → core}/modules/Response.js +15 -10
  15. package/src/backend/{_core → core}/modules/Response.php +14 -17
  16. package/src/backend/core/modules/Validate.js +56 -0
  17. package/src/backend/core/modules/Validate.php +52 -0
  18. package/src/backend/endpoints/protected/example-protected.js +39 -0
  19. package/src/backend/endpoints/protected/example-protected.php +47 -0
  20. package/src/backend/endpoints/public/example-public.js +39 -0
  21. package/src/backend/endpoints/public/example-public.php +47 -0
  22. package/src/backend/example.config.js +7 -6
  23. package/src/backend/example.config.php +8 -7
  24. package/src/backend/package.json +3 -3
  25. package/src/backend/web.config +2 -2
  26. package/src/frontend/hosting/.htaccess +1 -1
  27. package/src/frontend/hosting/web.config +1 -1
  28. package/src/frontend/layouts/base.njk +3 -7
  29. package/src/frontend/scss/{_global.scss → global.scss} +0 -2
  30. package/src/frontend/scss/modules/frameworks/_bootstrap.scss +0 -2
  31. package/src/frontend/scss/modules/frameworks/_bulma.scss +0 -2
  32. package/src/frontend/scss/pages/404.scss +0 -3
  33. package/src/frontend/scss/pages/homepage.scss +0 -3
  34. package/tools/res/templates/template.json +13 -0
  35. package/tools/res/templates/template.scss +0 -3
  36. package/src/backend/_core/index.js +0 -230
  37. package/src/backend/_core/index.php +0 -118
  38. package/src/backend/_core/init.js +0 -32
  39. package/src/backend/_core/init.php +0 -30
  40. package/src/backend/_core/modules/RateLimiter.js +0 -47
  41. package/src/backend/_core/modules/RateLimiter.php +0 -31
  42. package/src/backend/api/protected/example-protected.js +0 -16
  43. package/src/backend/api/protected/example-protected.php +0 -17
  44. package/src/backend/api/public/example-public.js +0 -16
  45. package/src/backend/api/public/example-public.php +0 -17
  46. package/src/frontend/scss/modules/frameworks/_foundation.scss +0 -138
  47. package/src/frontend/scss/modules/frameworks/_uikit.scss +0 -110
  48. package/tools/buildJs.js +0 -39
  49. /package/src/backend/{_core → core}/composer.json +0 -0
  50. /package/src/backend/{_core → core}/composer.lock +0 -0
@@ -1,230 +0,0 @@
1
- "use strict";
2
-
3
- const http = require("http");
4
- const fs = require("fs");
5
- const path = require("path");
6
- const crypto = require("crypto");
7
-
8
- const { createResponse, HALT } = require("./modules/Response");
9
- const RateLimiter = require("./modules/RateLimiter");
10
- const { loadConfig, handleException } = require("./init");
11
-
12
- const CORE_PATH = __dirname;
13
-
14
- const config = loadConfig();
15
-
16
- const APP_ENV = config.APP_ENV || "production";
17
-
18
- const DOCUMENT_ROOT =
19
- process.env.DOCUMENT_ROOT || path.join(CORE_PATH, "..", "..");
20
-
21
- const basePublic = path.join(CORE_PATH, "..", "api", "public");
22
- const baseProtected = path.join(CORE_PATH, "..", "api", "protected");
23
-
24
- function hashEquals(known, given) {
25
- const a = Buffer.from(String(known));
26
- const b = Buffer.from(String(given));
27
- if (a.length !== b.length) {
28
- return false;
29
- }
30
- return crypto.timingSafeEqual(a, b);
31
- }
32
-
33
- function send404(res) {
34
- res.statusCode = 404;
35
- const errorPage = DOCUMENT_ROOT ? path.join(DOCUMENT_ROOT, "404.html") : "";
36
- if (errorPage && fs.existsSync(errorPage)) {
37
- res.setHeader("Content-Type", "text/html; charset=UTF-8");
38
- res.end(fs.readFileSync(errorPage));
39
- } else {
40
- res.setHeader("Content-Type", "text/html; charset=UTF-8");
41
- res.end(
42
- "<h1>404 Not Found</h1>The requested URL was not found on this server.",
43
- );
44
- }
45
- }
46
-
47
- function readBody(req) {
48
- return new Promise((resolve) => {
49
- const chunks = [];
50
- req.on("data", (c) => chunks.push(c));
51
- req.on("end", () => {
52
- const raw = Buffer.concat(chunks).toString("utf8");
53
- if (!raw) return resolve({ raw: "", json: null });
54
- let json = null;
55
- try {
56
- json = JSON.parse(raw);
57
- } catch (e) {
58
- json = null;
59
- }
60
- resolve({ raw, json });
61
- });
62
- req.on("error", () => resolve({ raw: "", json: null }));
63
- });
64
- }
65
-
66
- async function handleRequest(req, res) {
67
- const Response = createResponse(res);
68
-
69
- try {
70
- const method = req.method;
71
- const parsedUrl = new URL(req.url, "http://localhost");
72
- let uri = parsedUrl.pathname;
73
-
74
- uri = uri.replace(/^\/api/, "");
75
- uri = uri.replace(/\/+$/, "");
76
- if (uri === "") uri = "/";
77
-
78
- const parts = uri.split("/").filter((p) => p !== "");
79
-
80
- for (const part of parts) {
81
- if (part === ".." || part === ".") {
82
- Response.error("Bad Request", 400);
83
- }
84
- }
85
-
86
- let endpointFile = null;
87
- let isProtected = false;
88
-
89
- const checkParts = parts.slice();
90
- const params = [];
91
-
92
- while (checkParts.length > 0) {
93
- const relativePath = checkParts.join("/") + ".js";
94
-
95
- const publicCandidate = path.join(basePublic, relativePath);
96
- if (fs.existsSync(publicCandidate)) {
97
- endpointFile = publicCandidate;
98
- isProtected = false;
99
- break;
100
- }
101
-
102
- const protectedCandidate = path.join(baseProtected, relativePath);
103
- if (fs.existsSync(protectedCandidate)) {
104
- endpointFile = protectedCandidate;
105
- isProtected = true;
106
- break;
107
- }
108
-
109
- params.unshift(checkParts.pop());
110
- }
111
-
112
- if (!endpointFile) {
113
- send404(res);
114
- return;
115
- }
116
-
117
- const base = isProtected ? baseProtected : basePublic;
118
- let endpointPath = endpointFile
119
- .split(path.sep)
120
- .join("/")
121
- .replace(base.split(path.sep).join("/"), "")
122
- .replace(/\.js$/, "")
123
- .replace(/^\//, "");
124
-
125
- res.setHeader("Content-Type", "application/json; charset=UTF-8");
126
- res.setHeader("X-Content-Type-Options", "nosniff");
127
- res.setHeader("X-Frame-Options", "DENY");
128
- res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
129
- res.setHeader(
130
- "Access-Control-Allow-Methods",
131
- "GET, POST, PUT, PATCH, DELETE, OPTIONS",
132
- );
133
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Api-Key");
134
-
135
- const originsForEndpoint =
136
- (config.CUSTOM_ENDPOINT_ORIGINS &&
137
- config.CUSTOM_ENDPOINT_ORIGINS[endpointPath]) ||
138
- config.GENERAL_ALLOWED_ORIGINS ||
139
- [];
140
-
141
- const origin = req.headers["origin"] || "";
142
-
143
- if (origin !== "" && originsForEndpoint.includes(origin)) {
144
- res.setHeader("Access-Control-Allow-Origin", origin);
145
- res.setHeader("Vary", "Origin");
146
- } else if (originsForEndpoint.includes("*")) {
147
- res.setHeader("Access-Control-Allow-Origin", "*");
148
- }
149
-
150
- if (method === "OPTIONS") {
151
- res.statusCode = 204;
152
- res.end();
153
- return;
154
- }
155
-
156
- // Rate limit: 60 requests / 60 seconds per IP
157
- const ip =
158
- (req.headers["x-forwarded-for"] || "").split(",")[0].trim() ||
159
- req.socket.remoteAddress ||
160
- "";
161
- RateLimiter.check(ip, 60, 60, Response, res);
162
-
163
- if (isProtected) {
164
- const validKey =
165
- (config.CUSTOM_ENDPOINT_KEYS &&
166
- config.CUSTOM_ENDPOINT_KEYS[endpointPath]) ||
167
- config.GENERAL_API_KEY ||
168
- "";
169
- const apiKey = req.headers["x-api-key"] || "";
170
-
171
- if (validKey === "" || !hashEquals(validKey, apiKey)) {
172
- Response.error("Unauthorized. X_API_KEY is incorrect or missing", 403);
173
- }
174
- }
175
-
176
- const handler = require(endpointFile);
177
-
178
- const context = {
179
- method, // HTTP method
180
- requestParams: params, // leftover URL segments (route params)
181
- Response, // request-bound Response helper
182
- query: Object.fromEntries(parsedUrl.searchParams), // parsed query string
183
- body: body.json, // parsed JSON body (or null)
184
- rawBody: body.raw, // raw body string
185
- headers: req.headers,
186
- req,
187
- res,
188
- config,
189
- CORE_PATH,
190
- };
191
-
192
- await handler(context);
193
-
194
- if (!res.writableEnded) {
195
- res.end();
196
- }
197
- } catch (err) {
198
- if (err === HALT) {
199
- return;
200
- }
201
- try {
202
- handleException(err, config, Response);
203
- } catch (inner) {
204
- if (inner === HALT) return;
205
- if (!res.writableEnded) {
206
- res.statusCode = 500;
207
- res.end(
208
- JSON.stringify({
209
- status: "error",
210
- message: "Internal server error",
211
- code: 500,
212
- }),
213
- );
214
- }
215
- }
216
- }
217
- }
218
-
219
- const HOST = process.env.HOST || "127.0.0.1";
220
- const PORT = parseInt(process.env.PORT || "3000", 10);
221
-
222
- const server = http.createServer(handleRequest);
223
-
224
- server.listen(PORT, HOST, () => {
225
- console.log(
226
- `[backend-node] listening on http://${HOST}:${PORT} (env: ${APP_ENV})`,
227
- );
228
- });
229
-
230
- module.exports = server;
@@ -1,118 +0,0 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- define('CORE_ACCESS', true);
6
- define('CORE_PATH', __DIR__);
7
-
8
- require_once __DIR__ . '/init.php';
9
- require_once __DIR__ . '/modules/Response.php';
10
-
11
- $method = $_SERVER['REQUEST_METHOD'];
12
- $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
13
-
14
- $uri = rtrim(preg_replace('#^/api#', '', $uri), '/') ?: '/';
15
- $parts = array_values(array_filter(explode('/', $uri)));
16
-
17
- foreach ($parts as $part) {
18
- if ($part === '..' || $part === '.') {
19
- Response::error('Bad Request', 400);
20
- }
21
- }
22
-
23
- $basePublic = __DIR__ . '/../api/public/';
24
- $baseProtected = __DIR__ . '/../api/protected/';
25
-
26
- $endpointFile = null;
27
- $isProtected = false;
28
- $requestParams = [];
29
- $checkParts = $parts;
30
- $params = [];
31
-
32
- while (count($checkParts) > 0) {
33
- $relativePath = implode('/', $checkParts) . '.php';
34
-
35
- // Check first whether this is a public route
36
- if (file_exists($basePublic . $relativePath)) {
37
- $endpointFile = $basePublic . $relativePath;
38
- $isProtected = false;
39
- break;
40
- }
41
-
42
- if (file_exists($baseProtected . $relativePath)) {
43
- $endpointFile = $baseProtected . $relativePath;
44
- $isProtected = true;
45
- break;
46
- }
47
-
48
- array_unshift($params, array_pop($checkParts));
49
- }
50
-
51
- if (!$endpointFile) {
52
- http_response_code(404);
53
- header_remove('WWW-Authenticate');
54
-
55
- $documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
56
- $errorPage = $documentRoot . '/404.html';
57
-
58
- if (!file_exists($errorPage)) {
59
- $errorPage = __DIR__ . '/../../404.html';
60
- }
61
-
62
- header('Content-Type: text/html; charset=UTF-8');
63
-
64
- if (file_exists($errorPage)) {
65
- echo file_get_contents($errorPage);
66
- } else {
67
- echo "<h1>404 Not Found</h1>";
68
- echo "The requested URL was not found on this server.";
69
- }
70
- exit;
71
- }
72
-
73
- $base = $isProtected ? $baseProtected : $basePublic;
74
- $base = str_replace('\\', '/', $base);
75
- $endpointPath = str_replace('\\', '/', $endpointFile);
76
- $endpointPath = preg_replace('#\.php$#', '', str_replace($base, '', $endpointPath));
77
-
78
- header('Content-Type: application/json; charset=UTF-8');
79
- header_remove('WWW-Authenticate');
80
- header('X-Content-Type-Options: nosniff');
81
- header('X-Frame-Options: DENY');
82
- header('Referrer-Policy: strict-origin-when-cross-origin');
83
- header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
84
- header('Access-Control-Allow-Headers: Content-Type, X-Api-Key');
85
-
86
- $originsForEndpoint = $config['CUSTOM_ENDPOINT_ORIGINS'][$endpointPath]
87
- ?? $config['GENERAL_ALLOWED_ORIGINS']
88
- ?? [];
89
-
90
- $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
91
-
92
- if ($origin !== '' && in_array($origin, $originsForEndpoint, true)) {
93
- header('Access-Control-Allow-Origin: ' . $origin);
94
- header('Vary: Origin');
95
- } elseif (in_array('*', $originsForEndpoint, true)) {
96
- header('Access-Control-Allow-Origin: *');
97
- }
98
-
99
- if ($method === 'OPTIONS') {
100
- http_response_code(204);
101
- exit;
102
- }
103
-
104
- require_once __DIR__ . '/modules/RateLimiter.php';
105
- RateLimiter::check($_SERVER['REMOTE_ADDR'] ?? '', 60, 60);
106
-
107
- if ($isProtected) {
108
- $validKey = $config['CUSTOM_ENDPOINT_KEYS'][$endpointPath] ?? $config['GENERAL_API_KEY'] ?? '';
109
- $apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
110
-
111
- if ($validKey === '' || !hash_equals($validKey, $apiKey)) {
112
- Response::error('Unauthorized. X_API_KEY is incorrect or missing', 403);
113
- }
114
- }
115
-
116
- $requestParams = $params;
117
-
118
- require $endpointFile;
@@ -1,32 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- function loadConfig() {
7
- const backendRoot = path.join(__dirname, "..");
8
- const configPath = path.join(backendRoot, "config.js");
9
- const examplePath = path.join(backendRoot, "example.config.js");
10
-
11
- if (fs.existsSync(configPath)) {
12
- return require(configPath);
13
- }
14
- return require(examplePath);
15
- }
16
-
17
- /**
18
- * Turn any thrown error into a 500 response, mirroring set_exception_handler.
19
- * @param {Error} exception
20
- * @param {object} config
21
- * @param {object} Response request-bound helper
22
- */
23
- function handleException(exception, config, Response) {
24
- const isDebug = (config.APP_ENV || "production") !== "production";
25
- Response.error(
26
- isDebug ? exception.message : "Internal server error",
27
- 500,
28
- isDebug ? { stack: exception.stack } : null,
29
- );
30
- }
31
-
32
- module.exports = { loadConfig, handleException };
@@ -1,30 +0,0 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- require_once __DIR__ . '/vendor/autoload.php';
6
- require_once __DIR__ . '/modules/Response.php';
7
-
8
- set_exception_handler(function ($exception) use (&$config) {
9
- $isDebug = ($config['APP_ENV'] ?? 'production') !== 'production';
10
- Response::error(
11
- $isDebug ? $exception->getMessage() : 'Internal server error',
12
- 500,
13
- $isDebug ? ['file' => $exception->getFile(), 'line' => $exception->getLine()] : null
14
- );
15
- });
16
-
17
- set_error_handler(function ($severity, $message, $file, $line) {
18
- if (!(error_reporting() & $severity)) return;
19
- throw new ErrorException($message, 0, $severity, $file, $line);
20
- });
21
-
22
- $config = require dirname(__DIR__) . '/config.php';
23
-
24
- if (($config['APP_ENV'] ?? 'production') === 'production') {
25
- ini_set('display_errors', '0');
26
- error_reporting(0);
27
- } else {
28
- ini_set('display_errors', '1');
29
- error_reporting(E_ALL);
30
- }
@@ -1,47 +0,0 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const crypto = require("crypto");
6
-
7
- class RateLimiter {
8
- /**
9
- * @param {string} ip
10
- * @param {number} maxRequests
11
- * @param {number} windowSeconds
12
- * @param {object} Response request-bound helper from createResponse(res)
13
- * @param {object} res raw http response (to set Retry-After)
14
- */
15
- static check(ip, maxRequests = 60, windowSeconds = 60, Response, res) {
16
- const cacheDir = path.join(__dirname, "..", "..", "cache");
17
-
18
- if (!fs.existsSync(cacheDir)) {
19
- fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
20
- }
21
-
22
- const hash = crypto.createHash("md5").update(ip).digest("hex");
23
- const cacheFile = path.join(cacheDir, "rl_" + hash + ".json");
24
- const now = Math.floor(Date.now() / 1000);
25
- let data = [];
26
-
27
- if (fs.existsSync(cacheFile)) {
28
- try {
29
- data = JSON.parse(fs.readFileSync(cacheFile, "utf8")) || [];
30
- } catch (e) {
31
- data = [];
32
- }
33
- }
34
-
35
- data = data.filter((ts) => ts > now - windowSeconds);
36
- data.push(now);
37
-
38
- fs.writeFileSync(cacheFile, JSON.stringify(data));
39
-
40
- if (data.length > maxRequests) {
41
- res.setHeader("Retry-After", String(windowSeconds));
42
- Response.error("Too Many Requests", 429);
43
- }
44
- }
45
- }
46
-
47
- module.exports = RateLimiter;
@@ -1,31 +0,0 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- class RateLimiter {
6
- public static function check(string $ip, int $maxRequests = 60, int $windowSeconds = 60): void {
7
- $cacheDir = __DIR__ . '/../../cache';
8
-
9
- if (!is_dir($cacheDir)) {
10
- mkdir($cacheDir, 0755, true);
11
- }
12
-
13
- $cacheFile = $cacheDir . '/rl_' . md5($ip) . '.json';
14
- $now = time();
15
- $data = [];
16
-
17
- if (file_exists($cacheFile)) {
18
- $data = json_decode(file_get_contents($cacheFile), true) ?: [];
19
- }
20
-
21
- $data = array_filter($data, fn($ts) => $ts > ($now - $windowSeconds));
22
- $data[] = $now;
23
-
24
- file_put_contents($cacheFile, json_encode(array_values($data)), LOCK_EX);
25
-
26
- if (count($data) > $maxRequests) {
27
- header('Retry-After: ' . $windowSeconds);
28
- Response::error('Too Many Requests', 429);
29
- }
30
- }
31
- }
@@ -1,16 +0,0 @@
1
- "use strict";
2
-
3
- module.exports = ({ method, requestParams, Response }) => {
4
- if (method !== "GET") {
5
- Response.error("Method not allowed", 405);
6
- }
7
-
8
- //
9
- // Your endpoint logic here. You can access route parameters in requestParams
10
- //
11
-
12
- Response.success({
13
- message: "Protected endpoint is working",
14
- params: requestParams,
15
- });
16
- };
@@ -1,17 +0,0 @@
1
- <?php
2
- declare(strict_types=1);
3
-
4
- require_once CORE_PATH . '/modules/Response.php';
5
-
6
- if ($method !== 'GET') {
7
- Response::error('Method not allowed', 405);
8
- }
9
-
10
- //
11
- // Your endpoint logic here. You can access route parameters in $requestParams array
12
- //
13
-
14
- Response::success([
15
- 'message' => 'Protected endpoint is working',
16
- 'params' => $requestParams,
17
- ]);
@@ -1,16 +0,0 @@
1
- "use strict";
2
-
3
- module.exports = ({ method, requestParams, Response }) => {
4
- if (method !== "GET") {
5
- Response.error("Method not allowed", 405);
6
- }
7
-
8
- //
9
- // Your endpoint logic here. You can access route parameters in requestParams
10
- //
11
-
12
- Response.success({
13
- message: "Public endpoint is working",
14
- params: requestParams,
15
- });
16
- };
@@ -1,17 +0,0 @@
1
- <?php
2
- declare(strict_types=1);
3
-
4
- require_once CORE_PATH . '/modules/Response.php';
5
-
6
- if ($method !== 'GET') {
7
- Response::error('Method not allowed', 405);
8
- }
9
-
10
- //
11
- // Your endpoint logic here. You can access route parameters in $requestParams array
12
- //
13
-
14
- Response::success([
15
- 'message' => 'Public endpoint is working',
16
- 'params' => $requestParams,
17
- ]);